Merge branch 'develop' of https://github.com/gcarq/freqtrade into develop

This commit is contained in:
chrisapril 2018-01-02 15:15:44 +01:00
commit 5fe416c1ce
37 changed files with 2086 additions and 406 deletions

11
.gitignore vendored
View File

@ -1,5 +1,10 @@
# Freqtrade rules # Freqtrade rules
freqtrade/tests/testdata/*.json freqtrade/tests/testdata/*.json
hyperopt_conf.py
config.json
*.sqlite
.hyperopt
logfile.txt
# Byte-compiled / optimized / DLL files # Byte-compiled / optimized / DLL files
__pycache__/ __pycache__/
@ -76,12 +81,6 @@ target/
# pyenv # pyenv
.python-version .python-version
config.json
preprocessor.py
*.sqlite
.hyperopt
logfile.txt
.env .env
.venv .venv
.idea .idea

View File

@ -1,4 +1,4 @@
sudo: false sudo: true
os: os:
- linux - linux
language: python language: python
@ -11,16 +11,27 @@ addons:
- libdw-dev - libdw-dev
- binutils-dev - binutils-dev
install: install:
- wget http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz - ./install_ta-lib.sh
- tar zxvf ta-lib-0.4.0-src.tar.gz
- cd ta-lib && ./configure && sudo make && sudo make install && cd ..
- export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH - export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH
- pip install flake8 coveralls - pip install flake8 coveralls
- pip install -r requirements.txt - pip install -r requirements.txt
script: - pip install -e .
- pytest --cov=freqtrade --cov-config=.coveragerc freqtrade/tests/ jobs:
include:
- script: pytest --cov=freqtrade --cov-config=.coveragerc freqtrade/tests/
- script:
- cp config.json.example config.json
- python freqtrade/main.py backtesting
- script:
- cp config.json.example config.json
- python freqtrade/main.py hyperopt -e 5
- script: flake8 freqtrade
after_success: after_success:
- flake8 freqtrade && coveralls - coveralls
notifications: notifications:
slack: slack:
secure: bKLXmOrx8e2aPZl7W8DA5BdPAXWGpI5UzST33oc1G/thegXcDVmHBTJrBs4sZak6bgAclQQrdZIsRd2eFYzHLalJEaw6pk7hoAw8SvLnZO0ZurWboz7qg2+aZZXfK4eKl/VUe4sM9M4e/qxjkK+yWG7Marg69c4v1ypF7ezUi1fPYILYw8u0paaiX0N5UX8XNlXy+PBlga2MxDjUY70MuajSZhPsY2pDUvYnMY1D/7XN3cFW0g+3O8zXjF0IF4q1Z/1ASQe+eYjKwPQacE+O8KDD+ZJYoTOFBAPllrtpO1jnOPFjNGf3JIbVMZw4bFjIL0mSQaiSUaUErbU3sFZ5Or79rF93XZ81V7uEZ55vD8KMfR2CB1cQJcZcj0v50BxLo0InkFqa0Y8Nra3sbpV4fV5Oe8pDmomPJrNFJnX6ULQhQ1gTCe0M5beKgVms5SITEpt4/Y0CmLUr6iHDT0CUiyMIRWAXdIgbGh1jfaWOMksybeRevlgDsIsNBjXmYI1Sw2ZZR2Eo2u4R6zyfyjOMLwYJ3vgq9IrACv2w5nmf0+oguMWHf6iWi2hiOqhlAN1W74+3HsYQcqnuM3LGOmuCnPprV1oGBqkPXjIFGpy21gNx4vHfO1noLUyJnMnlu2L7SSuN1CdLsnjJ1hVjpJjPfqB4nn8g12x87TqM1bOm+3Q= secure: bKLXmOrx8e2aPZl7W8DA5BdPAXWGpI5UzST33oc1G/thegXcDVmHBTJrBs4sZak6bgAclQQrdZIsRd2eFYzHLalJEaw6pk7hoAw8SvLnZO0ZurWboz7qg2+aZZXfK4eKl/VUe4sM9M4e/qxjkK+yWG7Marg69c4v1ypF7ezUi1fPYILYw8u0paaiX0N5UX8XNlXy+PBlga2MxDjUY70MuajSZhPsY2pDUvYnMY1D/7XN3cFW0g+3O8zXjF0IF4q1Z/1ASQe+eYjKwPQacE+O8KDD+ZJYoTOFBAPllrtpO1jnOPFjNGf3JIbVMZw4bFjIL0mSQaiSUaUErbU3sFZ5Or79rF93XZ81V7uEZ55vD8KMfR2CB1cQJcZcj0v50BxLo0InkFqa0Y8Nra3sbpV4fV5Oe8pDmomPJrNFJnX6ULQhQ1gTCe0M5beKgVms5SITEpt4/Y0CmLUr6iHDT0CUiyMIRWAXdIgbGh1jfaWOMksybeRevlgDsIsNBjXmYI1Sw2ZZR2Eo2u4R6zyfyjOMLwYJ3vgq9IrACv2w5nmf0+oguMWHf6iWi2hiOqhlAN1W74+3HsYQcqnuM3LGOmuCnPprV1oGBqkPXjIFGpy21gNx4vHfO1noLUyJnMnlu2L7SSuN1CdLsnjJ1hVjpJjPfqB4nn8g12x87TqM1bOm+3Q=
cache:
directories:
- $HOME/.cache/pip
- ta-lib

39
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,39 @@
Feel like our bot is missing a feature? We welcome your pull requests! Few pointers for contributions:
- Create your PR against the `develop` branch, not `master`.
- New features need to contain unit tests and must be PEP8 conformant (max-line-length = 100).
If you are unsure, discuss the feature on our [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE)
or in a [issue](https://github.com/gcarq/freqtrade/issues) before a PR.
Before sending the PR:
## Run unit tests
All unit tests must pass. If a unit test is broken, change your code to make it pass. It means you have introduced a regression
**Test the whole project**
```bash
pytest freqtrade
```
**Test only one file**
```bash
pytest freqtrade/tests/test_<file_name>.py
```
**Test only one method from one file**
```bash
pytest freqtrade/tests/test_<file_name>.py::test_<method_name>
```
## Test if your code is PEP8 compliant
**Install packages** (If not already installed)
```bash
pip3.6 install flake8 coveralls
```
**Run Flake8**
```bash
flake8 freqtrade
```

View File

@ -55,6 +55,12 @@ use the `last` price and values between those interpolate between ask and last
price. Using `ask` price will guarantee quick success in bid, but bot will also price. Using `ask` price will guarantee quick success in bid, but bot will also
end up paying more then would probably have been necessary. end up paying more then would probably have been necessary.
`fiat_display_currency` set the fiat to use for the conversion form coin to
fiat in Telegram. The valid value are: "AUD", "BRL", "CAD", "CHF",
"CLP", "CNY", "CZK", "DKK", "EUR", "GBP", "HKD", "HUF", "IDR", "ILS",
"INR", "JPY", "KRW", "MXN", "MYR", "NOK", "NZD", "PHP", "PKR", "PLN",
"RUB", "SEK", "SGD", "THB", "TRY", "TWD", "ZAR", "USD".
The other values should be self-explanatory, The other values should be self-explanatory,
if not feel free to raise a github issue. if not feel free to raise a github issue.
@ -62,6 +68,7 @@ if not feel free to raise a github issue.
* python3.6 * python3.6
* sqlite * sqlite
* [TA-lib](https://github.com/mrjbq7/ta-lib#dependencies) binaries * [TA-lib](https://github.com/mrjbq7/ta-lib#dependencies) binaries
* Minimal (advised) system requirements: 2GB RAM, 1GB data, 2vCPU
### Install ### Install
@ -253,8 +260,5 @@ $ pytest freqtrade
### Contributing ### Contributing
Feel like our bot is missing a feature? We welcome your pull requests! Few pointers for contributions: We welcome contributions. See our [contribution guide](https://github.com/gcarq/freqtrade/blob/develop/README.md)
for more details.
- Create your PR against the `develop` branch, not `master`.
- New features need to contain unit tests and must be PEP8 conform (`max-line-length = 100`).
- If you are unsure, discuss the feature on [slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE) or in a [issue](https://github.com/gcarq/freqtrade/issues) before a PR.

View File

@ -2,6 +2,7 @@
"max_open_trades": 3, "max_open_trades": 3,
"stake_currency": "BTC", "stake_currency": "BTC",
"stake_amount": 0.05, "stake_amount": 0.05,
"fiat_display_currency": "USD",
"dry_run": false, "dry_run": false,
"minimal_roi": { "minimal_roi": {
"40": 0.0, "40": 0.0,
@ -34,6 +35,9 @@
"BTC_POWR", "BTC_POWR",
"BTC_ADA", "BTC_ADA",
"BTC_XMR" "BTC_XMR"
],
"pair_blacklist": [
"BTC_DOGE"
] ]
}, },
"experimental": { "experimental": {

View File

@ -45,12 +45,16 @@ class Bittrex(Exchange):
Validates the given bittrex response Validates the given bittrex response
and raises a ContentDecodingError if a non-fatal issue happened. and raises a ContentDecodingError if a non-fatal issue happened.
""" """
if response['message'] == 'NO_API_RESPONSE': temp_error_messages = [
raise ContentDecodingError('Unable to decode bittrex response') 'NO_API_RESPONSE',
'MIN_TRADE_REQUIREMENT_NOT_MET',
]
if response['message'] in temp_error_messages:
raise ContentDecodingError('Got {}'.format(response['message']))
@property @property
def fee(self) -> float: def fee(self) -> float:
# See https://bittrex.com/fees # 0.25 %: See https://bittrex.com/fees
return 0.0025 return 0.0025
def buy(self, pair: str, rate: float, amount: float) -> str: def buy(self, pair: str, rate: float, amount: float) -> str:

156
freqtrade/fiat_convert.py Normal file
View File

@ -0,0 +1,156 @@
import logging
import time
from pymarketcap import Pymarketcap
logger = logging.getLogger(__name__)
class CryptoFiat():
# Constants
CACHE_DURATION = 6 * 60 * 60 # 6 hours
def __init__(self, crypto_symbol: str, fiat_symbol: str, price: float) -> None:
"""
Create an object that will contains the price for a crypto-currency in fiat
:param crypto_symbol: Crypto-currency you want to convert (e.g BTC)
:param fiat_symbol: FIAT currency you want to convert to (e.g USD)
:param price: Price in FIAT
"""
# Public attributes
self.crypto_symbol = None
self.fiat_symbol = None
self.price = 0.0
# Private attributes
self._expiration = 0
self.crypto_symbol = crypto_symbol.upper()
self.fiat_symbol = fiat_symbol.upper()
self.set_price(price=price)
def set_price(self, price: float) -> None:
"""
Set the price of the Crypto-currency in FIAT and set the expiration time
:param price: Price of the current Crypto currency in the fiat
:return: None
"""
self.price = price
self._expiration = time.time() + self.CACHE_DURATION
def is_expired(self) -> bool:
"""
Return if the current price is still valid or needs to be refreshed
:return: bool, true the price is expired and needs to be refreshed, false the price is
still valid
"""
return self._expiration - time.time() <= 0
class CryptoToFiatConverter():
# Constants
SUPPORTED_FIAT = [
"AUD", "BRL", "CAD", "CHF", "CLP", "CNY", "CZK", "DKK",
"EUR", "GBP", "HKD", "HUF", "IDR", "ILS", "INR", "JPY",
"KRW", "MXN", "MYR", "NOK", "NZD", "PHP", "PKR", "PLN",
"RUB", "SEK", "SGD", "THB", "TRY", "TWD", "ZAR", "USD"
]
def __init__(self) -> None:
self._coinmarketcap = Pymarketcap()
self._pairs = []
def convert_amount(self, crypto_amount: float, crypto_symbol: str, fiat_symbol: str) -> float:
"""
Convert an amount of crypto-currency to fiat
:param crypto_amount: amount of crypto-currency to convert
:param crypto_symbol: crypto-currency used
:param fiat_symbol: fiat to convert to
:return: float, value in fiat of the crypto-currency amount
"""
price = self.get_price(crypto_symbol=crypto_symbol, fiat_symbol=fiat_symbol)
return float(crypto_amount) * float(price)
def get_price(self, crypto_symbol: str, fiat_symbol: str) -> float:
"""
Return the price of the Crypto-currency in Fiat
:param crypto_symbol: Crypto-currency you want to convert (e.g BTC)
:param fiat_symbol: FIAT currency you want to convert to (e.g USD)
:return: Price in FIAT
"""
crypto_symbol = crypto_symbol.upper()
fiat_symbol = fiat_symbol.upper()
# Check if the fiat convertion you want is supported
if not self._is_supported_fiat(fiat=fiat_symbol):
raise ValueError('The fiat {} is not supported.'.format(fiat_symbol))
# Get the pair that interest us and return the price in fiat
for pair in self._pairs:
if pair.crypto_symbol == crypto_symbol and pair.fiat_symbol == fiat_symbol:
# If the price is expired we refresh it, avoid to call the API all the time
if pair.is_expired():
pair.set_price(
price=self._find_price(
crypto_symbol=pair.crypto_symbol,
fiat_symbol=pair.fiat_symbol
)
)
# return the last price we have for this pair
return pair.price
# The pair does not exist, so we create it and return the price
return self._add_pair(
crypto_symbol=crypto_symbol,
fiat_symbol=fiat_symbol,
price=self._find_price(
crypto_symbol=crypto_symbol,
fiat_symbol=fiat_symbol
)
)
def _add_pair(self, crypto_symbol: str, fiat_symbol: str, price: float) -> float:
"""
:param crypto_symbol: Crypto-currency you want to convert (e.g BTC)
:param fiat_symbol: FIAT currency you want to convert to (e.g USD)
:return: price in FIAT
"""
self._pairs.append(
CryptoFiat(
crypto_symbol=crypto_symbol,
fiat_symbol=fiat_symbol,
price=price
)
)
return price
def _is_supported_fiat(self, fiat: str) -> bool:
"""
Check if the FIAT your want to convert to is supported
:param fiat: FIAT to check (e.g USD)
:return: bool, True supported, False not supported
"""
fiat = fiat.upper()
return fiat in self.SUPPORTED_FIAT
def _find_price(self, crypto_symbol: str, fiat_symbol: str) -> float:
"""
Call CoinMarketCap API to retrieve the price in the FIAT
:param crypto_symbol: Crypto-currency you want to convert (e.g BTC)
:param fiat_symbol: FIAT currency you want to convert to (e.g USD)
:return: float, price of the crypto-currency in Fiat
"""
# Check if the fiat convertion you want is supported
if not self._is_supported_fiat(fiat=fiat_symbol):
raise ValueError('The fiat {} is not supported.'.format(fiat_symbol))
return float(
self._coinmarketcap.ticker(
currency=crypto_symbol,
convert=fiat_symbol
)['price_' + fiat_symbol.lower()]
)

View File

@ -17,25 +17,24 @@ from freqtrade.analyze import get_signal, SignalType
from freqtrade.misc import State, get_state, update_state, parse_args, throttle, \ from freqtrade.misc import State, get_state, update_state, parse_args, throttle, \
load_config load_config
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.fiat_convert import CryptoToFiatConverter
logger = logging.getLogger('freqtrade') logger = logging.getLogger('freqtrade')
_CONF = {} _CONF = {}
def refresh_whitelist(whitelist: Optional[List[str]] = None) -> None: def refresh_whitelist(whitelist: List[str]) -> List[str]:
""" """
Check wallet health and remove pair from whitelist if necessary Check wallet health and remove pair from whitelist if necessary
:param whitelist: a new whitelist (optional) :param whitelist: the pair the user might want to trade
:return: None :return: the list of pairs the user wants to trade without the one unavailable or black_listed
""" """
whitelist = whitelist or _CONF['exchange']['pair_whitelist']
sanitized_whitelist = [] sanitized_whitelist = []
health = exchange.get_wallet_health() health = exchange.get_wallet_health()
for status in health: for status in health:
pair = '{}_{}'.format(_CONF['stake_currency'], status['Currency']) pair = '{}_{}'.format(_CONF['stake_currency'], status['Currency'])
if pair not in whitelist: if pair not in whitelist or pair in _CONF['exchange'].get('pair_blacklist', []):
continue continue
if status['IsActive']: if status['IsActive']:
sanitized_whitelist.append(pair) sanitized_whitelist.append(pair)
@ -44,27 +43,29 @@ def refresh_whitelist(whitelist: Optional[List[str]] = None) -> None:
'Ignoring %s from whitelist (reason: %s).', 'Ignoring %s from whitelist (reason: %s).',
pair, status.get('Notice') or 'wallet is not active' pair, status.get('Notice') or 'wallet is not active'
) )
if _CONF['exchange']['pair_whitelist'] != sanitized_whitelist: return sanitized_whitelist
logger.debug('Using refreshed pair whitelist: %s ...', sanitized_whitelist)
_CONF['exchange']['pair_whitelist'] = sanitized_whitelist
def _process(dynamic_whitelist: Optional[int] = 0) -> bool: def _process(nb_assets: Optional[int] = 0) -> bool:
""" """
Queries the persistence layer for open trades and handles them, Queries the persistence layer for open trades and handles them,
otherwise a new trade is created. otherwise a new trade is created.
:param: dynamic_whitelist: True is a dynamic whitelist should be generated (optional) :param: nb_assets: the maximum number of pairs to be traded at the same time
:return: True if a trade has been created or closed, False otherwise :return: True if a trade has been created or closed, False otherwise
""" """
state_changed = False state_changed = False
try: try:
# Refresh whitelist based on wallet maintenance # Refresh whitelist based on wallet maintenance
refresh_whitelist( sanitized_list = refresh_whitelist(
gen_pair_whitelist( gen_pair_whitelist(
_CONF['stake_currency'], _CONF['stake_currency']
topn=dynamic_whitelist ) if nb_assets else _CONF['exchange']['pair_whitelist']
) if dynamic_whitelist else None
) )
# Keep only the subsets of pairs wanted (up to nb_assets)
final_list = sanitized_list[:nb_assets] if nb_assets else sanitized_list
_CONF['exchange']['pair_whitelist'] = final_list
# Query trades from persistence layer # Query trades from persistence layer
trades = Trade.query.filter(Trade.is_open.is_(True)).all() trades = Trade.query.filter(Trade.is_open.is_(True)).all()
if len(trades) < _CONF['max_open_trades']: if len(trades) < _CONF['max_open_trades']:
@ -76,8 +77,8 @@ def _process(dynamic_whitelist: Optional[int] = 0) -> bool:
'Checked all whitelisted currencies. ' 'Checked all whitelisted currencies. '
'Found no suitable entry positions for buying. Will keep looking ...' 'Found no suitable entry positions for buying. Will keep looking ...'
) )
except DependencyException as e: except DependencyException as exception:
logger.warning('Unable to create trade: %s', e) logger.warning('Unable to create trade: %s', exception)
for trade in trades: for trade in trades:
# Get order details for actual price per unit # Get order details for actual price per unit
@ -118,14 +119,44 @@ def execute_sell(trade: Trade, limit: float) -> None:
order_id = exchange.sell(str(trade.pair), limit, trade.amount) order_id = exchange.sell(str(trade.pair), limit, trade.amount)
trade.open_order_id = order_id trade.open_order_id = order_id
fmt_exp_profit = round(trade.calc_profit(limit) * 100, 2) fmt_exp_profit = round(trade.calc_profit_percent(rate=limit) * 100, 2)
rpc.send_msg('*{}:* Selling [{}]({}) with limit `{:.8f} (profit: ~{:.2f}%)`'.format( profit_trade = trade.calc_profit(rate=limit)
trade.exchange,
trade.pair.replace('_', '/'), message = '*{exchange}:* Selling [{pair}]({pair_url}) with limit `{limit:.8f}`'.format(
exchange.get_pair_detail_url(trade.pair), exchange=trade.exchange,
limit, pair=trade.pair.replace('_', '/'),
fmt_exp_profit pair_url=exchange.get_pair_detail_url(trade.pair),
)) limit=limit
)
# For regular case, when the configuration exists
if 'stake_currency' in _CONF and 'fiat_display_currency' in _CONF:
fiat_converter = CryptoToFiatConverter()
profit_fiat = fiat_converter.convert_amount(
profit_trade,
_CONF['stake_currency'],
_CONF['fiat_display_currency']
)
message += '` ({gain}: {profit_percent:.2f}%, {profit_coin:.8f} {coin}`' \
'` / {profit_fiat:.3f} {fiat})`'.format(
gain="profit" if fmt_exp_profit > 0 else "loss",
profit_percent=fmt_exp_profit,
profit_coin=profit_trade,
coin=_CONF['stake_currency'],
profit_fiat=profit_fiat,
fiat=_CONF['fiat_display_currency'],
)
# Because telegram._forcesell does not have the configuration
# Ignore the FIAT value and does not show the stake_currency as well
else:
message += '` ({gain}: {profit_percent:.2f}%, {profit_coin:.8f})`'.format(
gain="profit" if fmt_exp_profit > 0 else "loss",
profit_percent=fmt_exp_profit,
profit_coin=profit_trade
)
# Send the message
rpc.send_msg(message)
Trade.session.flush() Trade.session.flush()
@ -134,7 +165,7 @@ def min_roi_reached(trade: Trade, current_rate: float, current_time: datetime) -
Based an earlier trade and current price and ROI configuration, decides whether bot should sell Based an earlier trade and current price and ROI configuration, decides whether bot should sell
:return True if bot should sell at current rate :return True if bot should sell at current rate
""" """
current_profit = trade.calc_profit(current_rate) current_profit = trade.calc_profit_percent(current_rate)
if 'stoploss' in _CONF and current_profit < float(_CONF['stoploss']): if 'stoploss' in _CONF and current_profit < float(_CONF['stoploss']):
logger.debug('Stop loss hit.') logger.debug('Stop loss hit.')
return True return True
@ -158,7 +189,7 @@ def min_roi_reached(trade: Trade, current_rate: float, current_time: datetime) -
if time_diff > float(duration) and current_profit > threshold: if time_diff > float(duration) and current_profit > threshold:
return True return True
logger.debug('Threshold not reached. (cur_profit: %1.2f%%)', current_profit * 100.0) logger.debug('Threshold not reached. (cur_profit: %1.2f%%)', float(current_profit) * 100.0)
return False return False
@ -177,17 +208,20 @@ def handle_trade(trade: Trade) -> bool:
trade.update_stats(current_rate) trade.update_stats(current_rate)
# Check if minimal roi has been reached # Check if minimal roi has been reached
if not min_roi_reached(trade, current_rate, datetime.utcnow()): if min_roi_reached(trade, current_rate, datetime.utcnow()):
return False logger.debug('Executing sell due to ROI ...')
execute_sell(trade, current_rate)
return True
# Check if sell signal has been enabled and triggered # Check if sell signal has been enabled and triggered
if _CONF.get('experimental', {}).get('use_sell_signal'): if _CONF.get('experimental', {}).get('use_sell_signal'):
logger.debug('Checking sell_signal ...') logger.debug('Checking sell_signal ...')
if not get_signal(trade.pair, SignalType.SELL): if get_signal(trade.pair, SignalType.SELL):
return False logger.debug('Executing sell due to sell signal ...')
execute_sell(trade, current_rate)
return True
execute_sell(trade, current_rate) return False
return True
def get_target_bid(ticker: Dict[str, float]) -> float: def get_target_bid(ticker: Dict[str, float]) -> float:
@ -237,19 +271,28 @@ def create_trade(stake_amount: float) -> bool:
amount = stake_amount / buy_limit amount = stake_amount / buy_limit
order_id = exchange.buy(pair, buy_limit, amount) order_id = exchange.buy(pair, buy_limit, amount)
fiat_converter = CryptoToFiatConverter()
stake_amount_fiat = fiat_converter.convert_amount(
stake_amount,
_CONF['stake_currency'],
_CONF['fiat_display_currency']
)
# Create trade entity and return # Create trade entity and return
rpc.send_msg('*{}:* Buying [{}]({}) with limit `{:.8f}`'.format( rpc.send_msg('*{}:* Buying [{}]({}) with limit `{:.8f} ({:.6f} {}, {:.3f} {})` '.format(
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 buy_limit, 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 # Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
trade = Trade( trade = Trade(
pair=pair, pair=pair,
stake_amount=stake_amount, stake_amount=stake_amount,
amount=amount, amount=amount,
fee=exchange.get_fee() * 2, fee=exchange.get_fee(),
open_rate=buy_limit, open_rate=buy_limit,
open_date=datetime.utcnow(), open_date=datetime.utcnow(),
exchange=exchange.get_name().upper(), exchange=exchange.get_name().upper(),
@ -281,11 +324,10 @@ def init(config: dict, db_url: Optional[str] = None) -> None:
@cached(TTLCache(maxsize=1, ttl=1800)) @cached(TTLCache(maxsize=1, ttl=1800))
def gen_pair_whitelist(base_currency: str, topn: int = 20, key: str = 'BaseVolume') -> List[str]: def gen_pair_whitelist(base_currency: str, key: str = 'BaseVolume') -> List[str]:
""" """
Updates the whitelist with with a dynamically generated list Updates the whitelist with with a dynamically generated list
:param base_currency: base currency as str :param base_currency: base currency as str
:param topn: maximum number of returned results, must be greater than 0
:param key: sort key (defaults to 'BaseVolume') :param key: sort key (defaults to 'BaseVolume')
:return: List of pairs :return: List of pairs
""" """
@ -295,11 +337,7 @@ def gen_pair_whitelist(base_currency: str, topn: int = 20, key: str = 'BaseVolum
reverse=True reverse=True
) )
# topn must be greater than 0 return [s['MarketName'].replace('-', '_') for s in summaries]
if not topn > 0:
topn = 20
return [s['MarketName'].replace('-', '_') for s in summaries[:topn]]
def cleanup() -> None: def cleanup() -> None:
@ -370,7 +408,7 @@ def main() -> None:
throttle( throttle(
_process, _process,
min_secs=_CONF['internals'].get('process_throttle_secs', 10), min_secs=_CONF['internals'].get('process_throttle_secs', 10),
dynamic_whitelist=args.dynamic_whitelist, nb_assets=args.dynamic_whitelist,
) )
old_state = new_state old_state = new_state
except KeyboardInterrupt: except KeyboardInterrupt:

View File

@ -168,8 +168,8 @@ def build_subcommands(parser: argparse.ArgumentParser) -> None:
) )
backtesting_cmd.add_argument( backtesting_cmd.add_argument(
'-r', '--refresh-pairs-cached', '-r', '--refresh-pairs-cached',
help='refresh the pairs files in tests/testdata with the latest data from Bittrex. Use it if you want to \ help='refresh the pairs files in tests/testdata with the latest data from Bittrex. \
run your backtesting with up-to-date data.', Use it if you want to run your backtesting with up-to-date data.',
action='store_true', action='store_true',
dest='refresh_pairs', dest='refresh_pairs',
) )
@ -208,6 +208,7 @@ CONF_SCHEMA = {
'max_open_trades': {'type': 'integer', 'minimum': 1}, 'max_open_trades': {'type': 'integer', 'minimum': 1},
'stake_currency': {'type': 'string', 'enum': ['BTC', 'ETH', 'USDT']}, 'stake_currency': {'type': 'string', 'enum': ['BTC', 'ETH', 'USDT']},
'stake_amount': {'type': 'number', 'minimum': 0.0005}, 'stake_amount': {'type': 'number', 'minimum': 0.0005},
'fiat_display_currency': {'type': 'string', 'enum': ['USD', 'EUR', 'CAD', 'SGD']},
'dry_run': {'type': 'boolean'}, 'dry_run': {'type': 'boolean'},
'minimal_roi': { 'minimal_roi': {
'type': 'object', 'type': 'object',
@ -274,6 +275,14 @@ CONF_SCHEMA = {
'pattern': '^[0-9A-Z]+_[0-9A-Z]+$' 'pattern': '^[0-9A-Z]+_[0-9A-Z]+$'
}, },
'uniqueItems': True 'uniqueItems': True
},
'pair_blacklist': {
'type': 'array',
'items': {
'type': 'string',
'pattern': '^[0-9A-Z]+_[0-9A-Z]+$'
},
'uniqueItems': True
} }
}, },
'required': ['name', 'key', 'secret', 'pair_whitelist'] 'required': ['name', 'key', 'secret', 'pair_whitelist']
@ -286,6 +295,7 @@ CONF_SCHEMA = {
'max_open_trades', 'max_open_trades',
'stake_currency', 'stake_currency',
'stake_amount', 'stake_amount',
'fiat_display_currency',
'dry_run', 'dry_run',
'minimal_roi', 'minimal_roi',
'bid_strategy', 'bid_strategy',

View File

@ -4,16 +4,16 @@ import logging
import json import json
import os import os
from typing import Optional, List, Dict from typing import Optional, List, Dict
from freqtrade.exchange import get_ticker_history
from pandas import DataFrame from pandas import DataFrame
from freqtrade.exchange import get_ticker_history
from freqtrade.optimize.hyperopt_conf import hyperopt_optimize_conf
from freqtrade.analyze import populate_indicators, parse_ticker_dataframe from freqtrade.analyze import populate_indicators, parse_ticker_dataframe
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def load_data(pairs: List[str], ticker_interval: int = 5, refresh_pairs: Optional[bool] = False) -> Dict[str, List]: def load_data(ticker_interval: int = 5, pairs: Optional[List[str]] = None,
refresh_pairs: Optional[bool] = False) -> Dict[str, List]:
""" """
Loads ticker history data for the given parameters Loads ticker history data for the given parameters
:param ticker_interval: ticker interval in minutes :param ticker_interval: ticker interval in minutes
@ -23,12 +23,14 @@ def load_data(pairs: List[str], ticker_interval: int = 5, refresh_pairs: Optiona
path = testdata_path() path = testdata_path()
result = {} result = {}
_pairs = pairs or hyperopt_optimize_conf()['exchange']['pair_whitelist']
# If the user force the refresh of pairs # If the user force the refresh of pairs
if refresh_pairs: if refresh_pairs:
logger.info('Download data for all pairs and store them in freqtrade/tests/testsdata') logger.info('Download data for all pairs and store them in freqtrade/tests/testsdata')
download_pairs(pairs) download_pairs(_pairs)
for pair in pairs: for pair in _pairs:
file = '{abspath}/{pair}-{ticker_interval}.json'.format( file = '{abspath}/{pair}-{ticker_interval}.json'.format(
abspath=path, abspath=path,
pair=pair, pair=pair,
@ -46,25 +48,23 @@ def load_data(pairs: List[str], ticker_interval: int = 5, refresh_pairs: Optiona
def preprocess(tickerdata: Dict[str, List]) -> Dict[str, DataFrame]: def preprocess(tickerdata: Dict[str, List]) -> Dict[str, DataFrame]:
"""Creates a dataframe and populates indicators for given ticker data""" """Creates a dataframe and populates indicators for given ticker data"""
processed = {} return {pair: populate_indicators(parse_ticker_dataframe(pair_data))
for pair, pair_data in tickerdata.items(): for pair, pair_data in tickerdata.items()}
processed[pair] = populate_indicators(parse_ticker_dataframe(pair_data))
return processed
def testdata_path() -> str: def testdata_path() -> str:
"""Return the path where testdata files are stored""" """Return the path where testdata files are stored"""
return os.path.abspath(os.path.dirname(__file__)) + '/../tests/testdata' return os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'tests', 'testdata'))
def download_pairs(pairs: List[str]) -> bool: def download_pairs(pairs: List[str]) -> bool:
"""For each pairs passed in parameters, download 1 and 5 ticker intervals""" """For each pairs passed in parameters, download 1 and 5 ticker intervals"""
for pair in pairs: for pair in pairs:
try: try:
for interval in [1,5]: for interval in [1, 5]:
download_backtesting_testdata(pair=pair, interval=interval) download_backtesting_testdata(pair=pair, interval=interval)
except BaseException: except BaseException:
logger.info('Impossible to download the pair: "{pair}", Interval: {interval} min'.format( logger.info('Failed to download the pair: "{pair}", Interval: {interval} min'.format(
pair=pair, pair=pair,
interval=interval, interval=interval,
)) ))
@ -87,28 +87,28 @@ def download_backtesting_testdata(pair: str, interval: int = 5) -> bool:
)) ))
filepair = pair.replace("-", "_") filepair = pair.replace("-", "_")
filename = os.path.join(path, '{}-{}.json'.format( filename = os.path.join(path, '{pair}-{interval}.json'.format(
filepair, pair=filepair,
interval, interval=interval,
)) ))
filename = filename.replace('USDT_BTC', 'BTC_FAKEBULL') filename = filename.replace('USDT_BTC', 'BTC_FAKEBULL')
if os.path.isfile(filename): if os.path.isfile(filename):
with open(filename, "rt") as fp: with open(filename, "rt") as fp:
data = json.load(fp) data = json.load(fp)
logger.debug("Current Start:", data[1]['T']) logger.debug("Current Start: {}".format(data[1]['T']))
logger.debug("Current End: ", data[-1:][0]['T']) logger.debug("Current End: {}".format(data[-1:][0]['T']))
else: else:
data = [] data = []
logger.debug("Current Start: None") logger.debug("Current Start: None")
logger.debug("Current End: None") logger.debug("Current End: None")
new_data = get_ticker_history(pair = pair, tick_interval = int(interval)) new_data = get_ticker_history(pair=pair, tick_interval=int(interval))
for row in new_data: for row in new_data:
if row not in data: if row not in data:
data.append(row) data.append(row)
logger.debug("New Start:", data[1]['T']) logger.debug("New Start: {}".format(data[1]['T']))
logger.debug("New End: ", data[-1:][0]['T']) logger.debug("New End: {}".format(data[-1:][0]['T']))
data = sorted(data, key=lambda data: data['T']) data = sorted(data, key=lambda data: data['T'])
with open(filename, "wt") as fp: with open(filename, "wt") as fp:

View File

@ -48,8 +48,8 @@ def generate_text_table(
tabular_data.append([ tabular_data.append([
pair, pair,
len(result.index), len(result.index),
'{:.2f}%'.format(result.profit.mean() * 100.0), '{:.2f}%'.format(result.profit_percent.mean() * 100.0),
'{:.08f} {}'.format(result.profit.sum(), stake_currency), '{:.08f} {}'.format(result.profit_BTC.sum(), stake_currency),
'{:.2f}'.format(result.duration.mean() * ticker_interval), '{:.2f}'.format(result.duration.mean() * ticker_interval),
]) ])
@ -57,25 +57,25 @@ def generate_text_table(
tabular_data.append([ tabular_data.append([
'TOTAL', 'TOTAL',
len(results.index), len(results.index),
'{:.2f}%'.format(results.profit.mean() * 100.0), '{:.2f}%'.format(results.profit_percent.mean() * 100.0),
'{:.08f} {}'.format(results.profit.sum(), stake_currency), '{:.08f} {}'.format(results.profit_BTC.sum(), stake_currency),
'{:.2f}'.format(results.duration.mean() * ticker_interval), '{:.2f}'.format(results.duration.mean() * ticker_interval),
]) ])
return tabulate(tabular_data, headers=headers) return tabulate(tabular_data, headers=headers)
def backtest(config: Dict, processed: Dict[str, DataFrame], def backtest(stake_amount: float, processed: Dict[str, DataFrame],
max_open_trades: int = 0, realistic: bool = True) -> DataFrame: max_open_trades: int = 0, realistic: bool = True) -> DataFrame:
""" """
Implements backtesting functionality Implements backtesting functionality
:param config: config to use :param stake_amount: btc amount to use for each trade
:param processed: a processed dictionary with format {pair, data} :param processed: a processed dictionary with format {pair, data}
:param max_open_trades: maximum number of concurrent trades (default: 0, disabled) :param max_open_trades: maximum number of concurrent trades (default: 0, disabled)
:param realistic: do we try to simulate realistic trades? (default: True) :param realistic: do we try to simulate realistic trades? (default: True)
:return: DataFrame :return: DataFrame
""" """
trades = [] trades = []
trade_count_lock = {} trade_count_lock: dict = {}
exchange._API = Bittrex({'key': '', 'secret': ''}) exchange._API = Bittrex({'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
@ -98,8 +98,9 @@ def backtest(config: Dict, processed: Dict[str, DataFrame],
trade = Trade( trade = Trade(
open_rate=row.close, open_rate=row.close,
open_date=row.date, open_date=row.date,
amount=config['stake_amount'], stake_amount=stake_amount,
fee=exchange.get_fee() * 2 amount=stake_amount / row.open,
fee=exchange.get_fee()
) )
# calculate win/lose forwards from buy point # calculate win/lose forwards from buy point
@ -109,12 +110,20 @@ def backtest(config: Dict, processed: Dict[str, DataFrame],
trade_count_lock[row2.date] = trade_count_lock.get(row2.date, 0) + 1 trade_count_lock[row2.date] = trade_count_lock.get(row2.date, 0) + 1
if min_roi_reached(trade, row2.close, row2.date) or row2.sell == 1: if min_roi_reached(trade, row2.close, row2.date) or row2.sell == 1:
current_profit = trade.calc_profit(row2.close) current_profit_percent = trade.calc_profit_percent(rate=row2.close)
current_profit_btc = trade.calc_profit(rate=row2.close)
lock_pair_until = row2.Index lock_pair_until = row2.Index
trades.append((pair, current_profit, row2.Index - row.Index)) trades.append(
(
pair,
current_profit_percent,
current_profit_btc,
row2.Index - row.Index
)
)
break break
labels = ['currency', 'profit', 'duration'] labels = ['currency', 'profit_percent', 'profit_BTC', 'duration']
return DataFrame.from_records(trades, columns=labels) return DataFrame.from_records(trades, columns=labels)
@ -140,7 +149,8 @@ def start(args):
data[pair] = exchange.get_ticker_history(pair, args.ticker_interval) data[pair] = exchange.get_ticker_history(pair, args.ticker_interval)
else: else:
logger.info('Using local backtesting data (using whitelist in given config) ...') logger.info('Using local backtesting data (using whitelist in given config) ...')
data = load_data(pairs=pairs, ticker_interval=args.ticker_interval, refresh_pairs=args.refresh_pairs) data = load_data(pairs=pairs, ticker_interval=args.ticker_interval,
refresh_pairs=args.refresh_pairs)
logger.info('Using stake_currency: %s ...', config['stake_currency']) logger.info('Using stake_currency: %s ...', config['stake_currency'])
logger.info('Using stake_amount: %s ...', config['stake_amount']) logger.info('Using stake_amount: %s ...', config['stake_amount'])
@ -160,7 +170,7 @@ def start(args):
# Execute backtest and print results # Execute backtest and print results
results = backtest( results = backtest(
config, preprocess(data), max_open_trades, args.realistic_simulation config['stake_amount'], preprocess(data), max_open_trades, args.realistic_simulation
) )
logger.info( logger.info(
'\n====================== BACKTESTING REPORT ======================================\n%s', '\n====================== BACKTESTING REPORT ======================================\n%s',

View File

@ -1,4 +1,4 @@
# pragma pylint: disable=missing-docstring,W0212 # pragma pylint: disable=missing-docstring,W0212,W0603
import json import json
@ -8,7 +8,7 @@ from functools import reduce
from math import exp from math import exp
from operator import itemgetter from operator import itemgetter
from hyperopt import fmin, tpe, hp, Trials, STATUS_OK from hyperopt import fmin, tpe, hp, Trials, STATUS_OK, STATUS_FAIL
from hyperopt.mongoexp import MongoTrials from hyperopt.mongoexp import MongoTrials
from pandas import DataFrame from pandas import DataFrame
@ -16,6 +16,7 @@ from freqtrade import exchange, optimize
from freqtrade.exchange import Bittrex from freqtrade.exchange import Bittrex
from freqtrade.misc import load_config from freqtrade.misc import load_config
from freqtrade.optimize.backtesting import backtest from freqtrade.optimize.backtesting import backtest
from freqtrade.optimize.hyperopt_conf import hyperopt_optimize_conf
from freqtrade.vendor.qtpylib.indicators import crossed_above from freqtrade.vendor.qtpylib.indicators import crossed_above
# Remove noisy log messages # Remove noisy log messages
@ -24,30 +25,19 @@ logging.getLogger('hyperopt.tpe').setLevel(logging.WARNING)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# set TARGET_TRADES to suit your number concurrent trades so its realistic to 20days of data # set TARGET_TRADES to suit your number concurrent trades so its realistic to 20days of data
TARGET_TRADES = 1100 TARGET_TRADES = 1100
TOTAL_TRIES = None TOTAL_TRIES = None
_CURRENT_TRIES = 0 _CURRENT_TRIES = 0
CURRENT_BEST_LOSS = 100
TOTAL_PROFIT_TO_BEAT = 3 # this is expexted avg profit * expected trade count
AVG_PROFIT_TO_BEAT = 0.2 # for example 3.5%, 1100 trades, EXPECTED_MAX_PROFIT = 3.85
AVG_DURATION_TO_BEAT = 50 EXPECTED_MAX_PROFIT = 3.85
# Configuration and data used by hyperopt # Configuration and data used by hyperopt
PROCESSED = [] PROCESSED = optimize.preprocess(optimize.load_data())
OPTIMIZE_CONFIG = { OPTIMIZE_CONFIG = hyperopt_optimize_conf()
'max_open_trades': 3,
'stake_currency': 'BTC',
'stake_amount': 0.01,
'minimal_roi': {
'40': 0.0,
'30': 0.01,
'20': 0.02,
'0': 0.04,
},
'stoploss': -0.10,
}
# Monkey patch config # Monkey patch config
from freqtrade import main # noqa from freqtrade import main # noqa
@ -105,69 +95,70 @@ SPACE = {
def log_results(results): def log_results(results):
"if results is better than _TO_BEAT show it" """ log results if it is better than any previous evaluation """
global CURRENT_BEST_LOSS
current_try = results['current_tries'] if results['loss'] < CURRENT_BEST_LOSS:
total_tries = results['total_tries'] CURRENT_BEST_LOSS = results['loss']
result = results['result'] logger.info('{:5d}/{}: {}'.format(
profit = results['total_profit'] / 1000 results['current_tries'],
results['total_tries'],
outcome = '{:5d}/{}: {}'.format(current_try, total_tries, result) results['result']))
if profit >= TOTAL_PROFIT_TO_BEAT:
logger.info(outcome)
else: else:
print('.', end='') print('.', end='')
sys.stdout.flush() sys.stdout.flush()
def calculate_loss(total_profit: float, trade_count: int):
""" objective function, returns smaller number for more optimal results """
trade_loss = 1 - 0.35 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.2)
profit_loss = max(0, 1 - total_profit / EXPECTED_MAX_PROFIT)
return trade_loss + profit_loss
def optimizer(params): def optimizer(params):
global _CURRENT_TRIES global _CURRENT_TRIES
from freqtrade.optimize import backtesting from freqtrade.optimize import backtesting
backtesting.populate_buy_trend = buy_strategy_generator(params) backtesting.populate_buy_trend = buy_strategy_generator(params)
results = backtest(OPTIMIZE_CONFIG, PROCESSED) results = backtest(OPTIMIZE_CONFIG['stake_amount'], PROCESSED)
result_explanation = format_results(results)
result = format_results(results) total_profit = results.profit_percent.sum()
total_profit = results.profit.sum() * 1000
trade_count = len(results.index) trade_count = len(results.index)
trade_loss = 1 - 0.35 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.2) if trade_count == 0:
profit_loss = max(0, 1 - total_profit / 10000) # max profit 10000 print('.', end='')
return {
'status': STATUS_FAIL,
'loss': float('inf')
}
loss = calculate_loss(total_profit, trade_count)
_CURRENT_TRIES += 1 _CURRENT_TRIES += 1
result_data = { log_results({
'trade_count': trade_count, 'loss': loss,
'total_profit': total_profit,
'trade_loss': trade_loss,
'profit_loss': profit_loss,
'avg_profit': results.profit.mean() * 100.0,
'avg_duration': results.duration.mean() * 5,
'current_tries': _CURRENT_TRIES, 'current_tries': _CURRENT_TRIES,
'total_tries': TOTAL_TRIES, 'total_tries': TOTAL_TRIES,
'result': result, 'result': result_explanation,
'results': results })
}
# logger.info('{:5d}/{}: {}'.format(_CURRENT_TRIES, TOTAL_TRIES, result))
log_results(result_data)
return { return {
'loss': trade_loss + profit_loss, 'loss': loss,
'status': STATUS_OK, 'status': STATUS_OK,
'result': result 'result': result_explanation,
} }
def format_results(results: DataFrame): def format_results(results: DataFrame):
return ('Made {:6d} buys. Average profit {: 5.2f}%. ' return ('{:6d} trades. Avg profit {: 5.2f}%. '
'Total profit was {: 7.3f}. Average duration {:5.1f} mins.').format( 'Total profit {: 11.8f} BTC. Avg duration {:5.1f} mins.').format(
len(results.index), len(results.index),
results.profit.mean() * 100.0, results.profit_percent.mean() * 100.0,
results.profit.sum(), results.profit_BTC.sum(),
results.duration.mean() * 5, results.duration.mean() * 5,
) )
@ -226,13 +217,14 @@ def start(args):
# Initialize logger # Initialize logger
logging.basicConfig( logging.basicConfig(
level=args.loglevel, level=args.loglevel,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', format='\n%(message)s',
) )
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']
PROCESSED = optimize.preprocess(optimize.load_data(pairs=pairs, ticker_interval=args.ticker_interval)) PROCESSED = optimize.preprocess(optimize.load_data(
pairs=pairs, ticker_interval=args.ticker_interval))
if args.mongodb: if args.mongodb:
logger.info('Using mongodb ...') logger.info('Using mongodb ...')
@ -245,5 +237,6 @@ def start(args):
best = fmin(fn=optimizer, space=SPACE, algo=tpe.suggest, max_evals=TOTAL_TRIES, trials=trials) best = fmin(fn=optimizer, space=SPACE, algo=tpe.suggest, max_evals=TOTAL_TRIES, trials=trials)
logger.info('Best parameters:\n%s', json.dumps(best, indent=4)) logger.info('Best parameters:\n%s', json.dumps(best, indent=4))
results = sorted(trials.results, key=itemgetter('loss')) results = sorted(trials.results, key=itemgetter('loss'))
logger.info('Best Result:\n%s', results[0]['result']) logger.info('Best Result:\n%s', results[0]['result'])

View File

@ -0,0 +1,41 @@
"""
File that contains the configuration for Hyperopt
"""
def hyperopt_optimize_conf() -> dict:
"""
This function is used to define which parameters Hyperopt must used.
The "pair_whitelist" is only used is your are using Hyperopt with MongoDB,
without MongoDB, Hyperopt will use the pair your have set in your config file.
:return:
"""
return {
'max_open_trades': 3,
'stake_currency': 'BTC',
'stake_amount': 0.01,
"minimal_roi": {
'40': 0.0,
'30': 0.01,
'20': 0.02,
'0': 0.04,
},
'stoploss': -0.10,
"bid_strategy": {
"ask_last_balance": 0.0
},
"exchange": {
"pair_whitelist": [
"BTC_ETH",
"BTC_LTC",
"BTC_ETC",
"BTC_DASH",
"BTC_ZEC",
"BTC_XLM",
"BTC_NXT",
"BTC_POWR",
"BTC_ADA",
"BTC_XMR"
]
}
}

View File

@ -114,25 +114,27 @@ class Trade(_DECL_BASE):
return return
logger.info('Updating trade (id=%d) ...', self.id) logger.info('Updating trade (id=%d) ...', self.id)
getcontext().prec = 8 # Bittrex do not go above 8 decimal
if order['type'] == 'LIMIT_BUY': if order['type'] == 'LIMIT_BUY':
# Update open rate and actual amount # Update open rate and actual amount
self.open_rate = order['rate'] self.open_rate = Decimal(order['rate'])
self.amount = order['amount'] self.amount = Decimal(order['amount'])
logger.info('LIMIT_BUY has been fulfilled for %s.', self) logger.info('LIMIT_BUY has been fulfilled for %s.', self)
self.open_order_id = None self.open_order_id = None
elif order['type'] == 'LIMIT_SELL': elif order['type'] == 'LIMIT_SELL':
self.close(order['rate']) self.close(order['rate'])
else: else:
raise ValueError('Unknown order type: {}'.format(order['type'])) raise ValueError('Unknown order type: {}'.format(order['type']))
Trade.session.flush() cleanup()
def close(self, rate: float) -> None: def close(self, rate: float) -> None:
""" """
Sets close_rate to the given rate, calculates total profit Sets close_rate to the given rate, calculates total profit
and marks trade as closed and marks trade as closed
""" """
self.close_rate = rate self.close_rate = Decimal(rate)
self.close_profit = self.calc_profit() self.close_profit = self.calc_profit_percent()
self.close_date = datetime.utcnow() self.close_date = datetime.utcnow()
self.is_open = False self.is_open = False
self.open_order_id = None self.open_order_id = None
@ -141,7 +143,65 @@ class Trade(_DECL_BASE):
self self
) )
def calc_profit(self, rate: Optional[float] = None) -> float: def calc_open_trade_price(
self,
fee: Optional[float] = None) -> float:
"""
Calculate the open_rate in BTC
:param fee: fee to use on the open rate (optional).
If rate is not set self.fee will be used
:return: Price in BTC of the open trade
"""
getcontext().prec = 8
buy_trade = (Decimal(self.amount) * Decimal(self.open_rate))
fees = buy_trade * Decimal(fee or self.fee)
return float(buy_trade + fees)
def calc_close_trade_price(
self,
rate: Optional[float] = None,
fee: Optional[float] = None) -> float:
"""
Calculate the close_rate in BTC
:param fee: fee to use on the close rate (optional).
If rate is not set self.fee will be used
:param rate: rate to compare with (optional).
If rate is not set self.close_rate will be used
:return: Price in BTC of the open trade
"""
getcontext().prec = 8
if rate is None and not self.close_rate:
return 0.0
sell_trade = (Decimal(self.amount) * Decimal(rate or self.close_rate))
fees = sell_trade * Decimal(fee or self.fee)
return float(sell_trade - fees)
def calc_profit(
self,
rate: Optional[float] = None,
fee: Optional[float] = None) -> float:
"""
Calculate the profit in BTC between Close and Open trade
:param fee: fee to use on the close rate (optional).
If rate is not set self.fee will be used
:param rate: close rate to compare with (optional).
If rate is not set self.close_rate will be used
:return: profit in BTC as float
"""
open_trade_price = self.calc_open_trade_price()
close_trade_price = self.calc_close_trade_price(
rate=Decimal(rate or self.close_rate),
fee=Decimal(fee or self.fee)
)
return float("{0:.8f}".format(close_trade_price - open_trade_price))
def calc_profit_percent(
self,
rate: Optional[float] = None,
fee: Optional[float] = None) -> float:
""" """
Calculates the profit in percentage (including fee). Calculates the profit in percentage (including fee).
:param rate: rate to compare with (optional). :param rate: rate to compare with (optional).
@ -149,5 +209,11 @@ class Trade(_DECL_BASE):
:return: profit in percentage as float :return: profit in percentage as float
""" """
getcontext().prec = 8 getcontext().prec = 8
return float((Decimal(rate or self.close_rate) - Decimal(self.open_rate))
/ Decimal(self.open_rate) - Decimal(self.fee)) open_trade_price = self.calc_open_trade_price()
close_trade_price = self.calc_close_trade_price(
rate=Decimal(rate or self.close_rate),
fee=Decimal(fee or self.fee)
)
return float("{0:.8f}".format((close_trade_price / open_trade_price) - 1))

View File

@ -1,12 +1,12 @@
import logging import logging
import re import re
from datetime import timedelta, date
from decimal import Decimal from decimal import Decimal
from datetime import timedelta, datetime
from typing import Callable, Any from typing import Callable, Any
import arrow import arrow
from pandas import DataFrame from pandas import DataFrame
from sqlalchemy import and_, func, text, between from sqlalchemy import and_, func, text
from tabulate import tabulate from tabulate import tabulate
from telegram import ParseMode, Bot, Update, ReplyKeyboardMarkup from telegram import ParseMode, Bot, Update, ReplyKeyboardMarkup
from telegram.error import NetworkError, TelegramError from telegram.error import NetworkError, TelegramError
@ -15,6 +15,7 @@ from telegram.ext import CommandHandler, Updater
from freqtrade import exchange, __version__ from freqtrade import exchange, __version__
from freqtrade.misc import get_state, State, update_state from freqtrade.misc import get_state, State, update_state
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.fiat_convert import CryptoToFiatConverter
# Remove noisy log messages # Remove noisy log messages
logging.getLogger('requests.packages.urllib3').setLevel(logging.INFO) logging.getLogger('requests.packages.urllib3').setLevel(logging.INFO)
@ -23,6 +24,7 @@ logger = logging.getLogger(__name__)
_UPDATER: Updater = None _UPDATER: Updater = None
_CONF = {} _CONF = {}
_FIAT_CONVERT = CryptoToFiatConverter()
def init(config: dict) -> None: def init(config: dict) -> None:
@ -139,7 +141,7 @@ def _status(bot: Bot, update: Update) -> None:
order = exchange.get_order(trade.open_order_id) order = exchange.get_order(trade.open_order_id)
# calculate profit and send message to user # calculate profit and send message to user
current_rate = exchange.get_ticker(trade.pair)['bid'] current_rate = exchange.get_ticker(trade.pair)['bid']
current_profit = trade.calc_profit(current_rate) current_profit = trade.calc_profit_percent(current_rate)
fmt_close_profit = '{:.2f}%'.format( fmt_close_profit = '{:.2f}%'.format(
round(trade.close_profit * 100, 2) round(trade.close_profit * 100, 2)
) if trade.close_profit else None ) if trade.close_profit else None
@ -196,7 +198,7 @@ def _status_table(bot: Bot, update: Update) -> None:
trade.id, trade.id,
trade.pair, trade.pair,
shorten_date(arrow.get(trade.open_date).humanize(only_distance=True)), shorten_date(arrow.get(trade.open_date).humanize(only_distance=True)),
'{:.2f}'.format(100 * trade.calc_profit(current_rate)) '{:.2f}%'.format(100 * trade.calc_profit_percent(current_rate))
]) ])
columns = ['ID', 'Pair', 'Since', 'Profit'] columns = ['ID', 'Pair', 'Since', 'Profit']
@ -218,28 +220,51 @@ def _daily(bot: Bot, update: Update) -> None:
:param update: message update :param update: message update
:return: None :return: None
""" """
today = date.today().toordinal() today = datetime.utcnow().date()
profit_days = {} profit_days = {}
try: try:
timescale = int(update.message.text.replace('/daily', '').strip()) timescale = int(update.message.text.replace('/daily', '').strip())
except (TypeError, ValueError): except (TypeError, ValueError):
timescale = 5 timescale = 7
if not (isinstance(timescale, int) and timescale > 0): if not (isinstance(timescale, int) and timescale > 0):
send_msg('*Daily [n]:* `must be an integer greater than 0`', bot=bot) send_msg('*Daily [n]:* `must be an integer greater than 0`', bot=bot)
return return
for day in range(0, timescale): for day in range(0, timescale):
# need to query between day+1 and day-1 profitday = today - timedelta(days=day)
nextdate = date.fromordinal(today-day+1) trades = Trade.query \
prevdate = date.fromordinal(today-day-1) .filter(Trade.is_open.is_(False)) \
trades = Trade.query.filter(between(Trade.close_date, prevdate, nextdate)).all() .filter(Trade.close_date >= profitday)\
curdayprofit = sum(trade.close_profit * trade.stake_amount for trade in trades) .filter(Trade.close_date < (profitday + timedelta(days=1)))\
profit_days[date.fromordinal(today-day)] = format(curdayprofit, '.8f') .order_by(Trade.close_date)\
.all()
curdayprofit = sum(trade.calc_profit() for trade in trades)
profit_days[profitday] = format(curdayprofit, '.8f')
stats = [[key, str(value) + ' BTC'] for key, value in profit_days.items()] stats = [
stats = tabulate(stats, headers=['Day', 'Profit'], tablefmt='simple') [
key,
'{value:.8f} {symbol}'.format(value=float(value), symbol=_CONF['stake_currency']),
'{value:.3f} {symbol}'.format(
value=_FIAT_CONVERT.convert_amount(
value,
_CONF['stake_currency'],
_CONF['fiat_display_currency']
),
symbol=_CONF['fiat_display_currency']
)
]
for key, value in profit_days.items()
]
stats = tabulate(stats,
headers=[
'Day',
'Profit {}'.format(_CONF['stake_currency']),
'Profit {}'.format(_CONF['fiat_display_currency'])
],
tablefmt='simple')
message = '<b>Daily Profit over the last {} days</b>:\n<pre>{}</pre>'.format(timescale, stats) message = '<b>Daily Profit over the last {} days</b>:\n<pre>{}</pre>'.format(timescale, stats)
send_msg(message, bot=bot, parse_mode=ParseMode.HTML) send_msg(message, bot=bot, parse_mode=ParseMode.HTML)
@ -256,10 +281,10 @@ def _profit(bot: Bot, update: Update) -> None:
""" """
trades = Trade.query.order_by(Trade.id).all() trades = Trade.query.order_by(Trade.id).all()
profit_all_btc = [] profit_all_coin = []
profit_all = [] profit_all_percent = []
profit_btc_closed = [] profit_closed_coin = []
profit_closed = [] profit_closed_percent = []
durations = [] durations = []
for trade in trades: for trade in trades:
@ -271,16 +296,16 @@ def _profit(bot: Bot, update: Update) -> None:
durations.append((trade.close_date - trade.open_date).total_seconds()) durations.append((trade.close_date - trade.open_date).total_seconds())
if not trade.is_open: if not trade.is_open:
profit = trade.close_profit profit_percent = trade.calc_profit_percent()
profit_btc_closed.append(Decimal(trade.close_rate) - Decimal(trade.open_rate)) profit_closed_coin.append(trade.calc_profit())
profit_closed.append(profit) profit_closed_percent.append(profit_percent)
else: else:
# Get current rate # Get current rate
current_rate = exchange.get_ticker(trade.pair)['bid'] current_rate = exchange.get_ticker(trade.pair)['bid']
profit = trade.calc_profit(current_rate) profit_percent = trade.calc_profit_percent(rate=current_rate)
profit_all_btc.append(Decimal(trade.close_rate or current_rate) - Decimal(trade.open_rate)) profit_all_coin.append(trade.calc_profit(rate=Decimal(trade.close_rate or current_rate)))
profit_all.append(profit) profit_all_percent.append(profit_percent)
best_pair = Trade.session.query(Trade.pair, func.sum(Trade.close_profit).label('profit_sum')) \ best_pair = Trade.session.query(Trade.pair, func.sum(Trade.close_profit).label('profit_sum')) \
.filter(Trade.is_open.is_(False)) \ .filter(Trade.is_open.is_(False)) \
@ -293,19 +318,46 @@ def _profit(bot: Bot, update: Update) -> None:
return return
bp_pair, bp_rate = best_pair bp_pair, bp_rate = best_pair
# Prepare data to display
profit_closed_coin = round(sum(profit_closed_coin), 8)
profit_closed_percent = round(sum(profit_closed_percent) * 100, 2)
profit_closed_fiat = _FIAT_CONVERT.convert_amount(
profit_closed_coin,
_CONF['stake_currency'],
_CONF['fiat_display_currency']
)
profit_all_coin = round(sum(profit_all_coin), 8)
profit_all_percent = round(sum(profit_all_percent) * 100, 2)
profit_all_fiat = _FIAT_CONVERT.convert_amount(
profit_all_coin,
_CONF['stake_currency'],
_CONF['fiat_display_currency']
)
# Message to display
markdown_msg = """ markdown_msg = """
*ROI Trade closed:* `{profit_closed_btc:.8f} BTC ({profit_closed:.2f}%)` *ROI:* Close trades
*ROI All trades:* `{profit_all_btc:.8f} BTC ({profit_all:.2f}%)` `{profit_closed_coin:.8f} {coin} ({profit_closed_percent:.2f}%)`
`{profit_closed_fiat:.3f} {fiat}`
*ROI:* All trades
`{profit_all_coin:.8f} {coin} ({profit_all_percent:.2f}%)`
`{profit_all_fiat:.3f} {fiat}`
*Total Trade Count:* `{trade_count}` *Total Trade Count:* `{trade_count}`
*First Trade opened:* `{first_trade_date}` *First Trade opened:* `{first_trade_date}`
*Latest Trade opened:* `{latest_trade_date}` *Latest Trade opened:* `{latest_trade_date}`
*Avg. Duration:* `{avg_duration}` *Avg. Duration:* `{avg_duration}`
*Best Performing:* `{best_pair}: {best_rate:.2f}%` *Best Performing:* `{best_pair}: {best_rate:.2f}%`
""".format( """.format(
profit_closed_btc=round(sum(profit_btc_closed), 8), coin=_CONF['stake_currency'],
profit_closed=round(sum(profit_closed) * 100, 2), fiat=_CONF['fiat_display_currency'],
profit_all_btc=round(sum(profit_all_btc), 8), profit_closed_coin=profit_closed_coin,
profit_all=round(sum(profit_all) * 100, 2), profit_closed_percent=profit_closed_percent,
profit_closed_fiat=profit_closed_fiat,
profit_all_coin=profit_all_coin,
profit_all_percent=profit_all_percent,
profit_all_fiat=profit_all_fiat,
trade_count=len(trades), trade_count=len(trades),
first_trade_date=arrow.get(trades[0].open_date).humanize(), first_trade_date=arrow.get(trades[0].open_date).humanize(),
latest_trade_date=arrow.get(trades[-1].open_date).humanize(), latest_trade_date=arrow.get(trades[-1].open_date).humanize(),

View File

@ -15,7 +15,8 @@ def default_conf():
configuration = { configuration = {
"max_open_trades": 1, "max_open_trades": 1,
"stake_currency": "BTC", "stake_currency": "BTC",
"stake_amount": 0.05, "stake_amount": 0.001,
"fiat_display_currency": "USD",
"dry_run": True, "dry_run": True,
"minimal_roi": { "minimal_roi": {
"40": 0.0, "40": 0.0,
@ -61,9 +62,27 @@ def update():
@pytest.fixture @pytest.fixture
def ticker(): def ticker():
return MagicMock(return_value={ return MagicMock(return_value={
'bid': 0.07256061, 'bid': 0.00001098,
'ask': 0.072661, 'ask': 0.00001099,
'last': 0.07256061, 'last': 0.00001098,
})
@pytest.fixture
def ticker_sell_up():
return MagicMock(return_value={
'bid': 0.00001172,
'ask': 0.00001173,
'last': 0.00001172,
})
@pytest.fixture
def ticker_sell_down():
return MagicMock(return_value={
'bid': 0.00001044,
'ask': 0.00001043,
'last': 0.00001044,
}) })
@ -104,8 +123,8 @@ def limit_buy_order():
'type': 'LIMIT_BUY', 'type': 'LIMIT_BUY',
'pair': 'mocked', 'pair': 'mocked',
'opened': datetime.utcnow(), 'opened': datetime.utcnow(),
'rate': 0.07256061, 'rate': 0.00001099,
'amount': 206.43811673387373, 'amount': 90.99181073,
'remaining': 0.0, 'remaining': 0.0,
'closed': datetime.utcnow(), 'closed': datetime.utcnow(),
} }
@ -118,8 +137,8 @@ def limit_sell_order():
'type': 'LIMIT_SELL', 'type': 'LIMIT_SELL',
'pair': 'mocked', 'pair': 'mocked',
'opened': datetime.utcnow(), 'opened': datetime.utcnow(),
'rate': 0.0802134, 'rate': 0.00001173,
'amount': 206.43811673387373, 'amount': 90.99181073,
'remaining': 0.0, 'remaining': 0.0,
'closed': datetime.utcnow(), 'closed': datetime.utcnow(),
} }
@ -128,7 +147,7 @@ def limit_sell_order():
@pytest.fixture @pytest.fixture
def ticker_history(): def ticker_history():
return [ return [
{ {
"O": 8.794e-05, "O": 8.794e-05,
"H": 8.948e-05, "H": 8.948e-05,
"L": 8.794e-05, "L": 8.794e-05,
@ -137,7 +156,7 @@ def ticker_history():
"T": "2017-11-26T08:50:00", "T": "2017-11-26T08:50:00",
"BV": 0.0877869 "BV": 0.0877869
}, },
{ {
"O": 8.88e-05, "O": 8.88e-05,
"H": 8.942e-05, "H": 8.942e-05,
"L": 8.88e-05, "L": 8.88e-05,
@ -146,7 +165,7 @@ def ticker_history():
"T": "2017-11-26T08:55:00", "T": "2017-11-26T08:55:00",
"BV": 0.05874751 "BV": 0.05874751
}, },
{ {
"O": 8.891e-05, "O": 8.891e-05,
"H": 8.893e-05, "H": 8.893e-05,
"L": 8.875e-05, "L": 8.875e-05,

View File

@ -0,0 +1,188 @@
# pragma pylint: disable=missing-docstring,C0103
from unittest.mock import MagicMock
from requests.exceptions import RequestException
from random import randint
import logging
import pytest
from freqtrade import OperationalException
from freqtrade.exchange import init, validate_pairs, buy, sell, get_balance, get_balances, \
get_ticker, cancel_order, get_name, get_fee
def test_init(default_conf, mocker, caplog):
mocker.patch('freqtrade.exchange.validate_pairs', side_effect=lambda s: True)
init(config=default_conf)
assert ('freqtrade.exchange',
logging.INFO,
'Instance is running with dry_run enabled'
) in caplog.record_tuples
def test_init_exception(default_conf, mocker):
default_conf['exchange']['name'] = 'wrong_exchange_name'
with pytest.raises(
OperationalException,
match='Exchange {} is not supported'.format(default_conf['exchange']['name'])):
init(config=default_conf)
def test_validate_pairs(default_conf, mocker):
api_mock = MagicMock()
api_mock.get_markets = MagicMock(return_value=[
'BTC_ETH', 'BTC_TKN', 'BTC_TRST', 'BTC_SWT', 'BTC_BCC',
])
mocker.patch('freqtrade.exchange._API', api_mock)
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
validate_pairs(default_conf['exchange']['pair_whitelist'])
def test_validate_pairs_not_available(default_conf, mocker):
api_mock = MagicMock()
api_mock.get_markets = MagicMock(return_value=[])
mocker.patch('freqtrade.exchange._API', api_mock)
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
with pytest.raises(OperationalException, match=r'not available'):
validate_pairs(default_conf['exchange']['pair_whitelist'])
def test_validate_pairs_not_compatible(default_conf, mocker):
api_mock = MagicMock()
api_mock.get_markets = MagicMock(return_value=['BTC_ETH', 'BTC_TKN', 'BTC_TRST', 'BTC_SWT'])
default_conf['stake_currency'] = 'ETH'
mocker.patch('freqtrade.exchange._API', api_mock)
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
with pytest.raises(OperationalException, match=r'not compatible'):
validate_pairs(default_conf['exchange']['pair_whitelist'])
def test_validate_pairs_exception(default_conf, mocker, caplog):
api_mock = MagicMock()
api_mock.get_markets = MagicMock(side_effect=RequestException())
mocker.patch('freqtrade.exchange._API', api_mock)
# with pytest.raises(RequestException, match=r'Unable to validate pairs'):
validate_pairs(default_conf['exchange']['pair_whitelist'])
assert ('freqtrade.exchange',
logging.WARNING,
'Unable to validate pairs (assuming they are correct). Reason: '
) in caplog.record_tuples
def test_buy_dry_run(default_conf, mocker):
default_conf['dry_run'] = True
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
assert 'dry_run_buy_' in buy(pair='BTC_ETH', rate=200, amount=1)
def test_buy_prod(default_conf, mocker):
api_mock = MagicMock()
api_mock.buy = MagicMock(return_value='dry_run_buy_{}'.format(randint(0, 10**6)))
mocker.patch('freqtrade.exchange._API', api_mock)
default_conf['dry_run'] = False
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
assert 'dry_run_buy_' in buy(pair='BTC_ETH', rate=200, amount=1)
def test_sell_dry_run(default_conf, mocker):
default_conf['dry_run'] = True
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
assert 'dry_run_sell_' in sell(pair='BTC_ETH', rate=200, amount=1)
def test_sell_prod(default_conf, mocker):
api_mock = MagicMock()
api_mock.sell = MagicMock(return_value='dry_run_sell_{}'.format(randint(0, 10**6)))
mocker.patch('freqtrade.exchange._API', api_mock)
default_conf['dry_run'] = False
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
assert 'dry_run_sell_' in sell(pair='BTC_ETH', rate=200, amount=1)
def test_get_balance_dry_run(default_conf, mocker):
default_conf['dry_run'] = True
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
assert get_balance(currency='BTC') == 999.9
def test_get_balance_prod(default_conf, mocker):
api_mock = MagicMock()
api_mock.get_balance = MagicMock(return_value=123.4)
mocker.patch('freqtrade.exchange._API', api_mock)
default_conf['dry_run'] = False
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
assert get_balance(currency='BTC') == 123.4
def test_get_balances_dry_run(default_conf, mocker):
default_conf['dry_run'] = True
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
assert get_balances() == []
def test_get_balances_prod(default_conf, mocker):
balance_item = {
'Currency': '1ST',
'Balance': 10.0,
'Available': 10.0,
'Pending': 0.0,
'CryptoAddress': None
}
api_mock = MagicMock()
api_mock.get_balances = MagicMock(return_value=[balance_item, balance_item, balance_item])
mocker.patch('freqtrade.exchange._API', api_mock)
default_conf['dry_run'] = False
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
assert len(get_balances()) == 3
assert get_balances()[0]['Currency'] == '1ST'
assert get_balances()[0]['Balance'] == 10.0
assert get_balances()[0]['Available'] == 10.0
assert get_balances()[0]['Pending'] == 0.0
def test_get_ticker(mocker, ticker):
api_mock = MagicMock()
api_mock.get_ticker = MagicMock(return_value=ticker())
mocker.patch('freqtrade.exchange._API', api_mock)
ticker = get_ticker(pair='BTC_ETH')
assert ticker['bid'] == 0.00001098
assert ticker['ask'] == 0.00001099
assert ticker['bid'] == 0.00001098
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
def test_get_name(default_conf, mocker):
mocker.patch('freqtrade.exchange.validate_pairs', side_effect=lambda s: True)
default_conf['exchange']['name'] = 'bittrex'
init(default_conf)
assert get_name() == 'Bittrex'
def test_get_fee(default_conf, mocker):
mocker.patch('freqtrade.exchange.validate_pairs', side_effect=lambda s: True)
init(default_conf)
assert get_fee() == 0.0025

View File

@ -0,0 +1,32 @@
# pragma pylint: disable=missing-docstring,C0103
import pytest
from requests.exceptions import ContentDecodingError
from freqtrade.exchange import Bittrex
def test_validate_response_success():
response = {
'message': '',
'result': [],
}
Bittrex._validate_response(response)
def test_validate_response_no_api_response():
response = {
'message': 'NO_API_RESPONSE',
'result': None,
}
with pytest.raises(ContentDecodingError, match=r'.*NO_API_RESPONSE.*'):
Bittrex._validate_response(response)
def test_validate_response_min_trade_requirement_not_met():
response = {
'message': 'MIN_TRADE_REQUIREMENT_NOT_MET',
'result': None,
}
with pytest.raises(ContentDecodingError, match=r'.*MIN_TRADE_REQUIREMENT_NOT_MET.*'):
Bittrex._validate_response(response)

View File

@ -0,0 +1,159 @@
# pragma pylint: disable=missing-docstring,W0212
import math
import pandas as pd
# from unittest.mock import MagicMock
from freqtrade import exchange, optimize
from freqtrade.exchange import Bittrex
from freqtrade.optimize.backtesting import backtest, generate_text_table, get_timeframe
# import freqtrade.optimize.backtesting as backtesting
def test_generate_text_table():
results = pd.DataFrame(
{
'currency': ['BTC_ETH', 'BTC_ETH'],
'profit_percent': [0.1, 0.2],
'profit_BTC': [0.2, 0.4],
'duration': [10, 30]
}
)
assert generate_text_table({'BTC_ETH': {}}, results, 'BTC', 5) == (
'pair buy count avg profit total profit avg duration\n'
'------- ----------- ------------ -------------- --------------\n'
'BTC_ETH 2 15.00% 0.60000000 BTC 100\n'
'TOTAL 2 15.00% 0.60000000 BTC 100')
def test_get_timeframe():
data = optimize.load_data(ticker_interval=1, pairs=['BTC_UNITEST'])
min_date, max_date = get_timeframe(data)
assert min_date.isoformat() == '2017-11-04T23:02:00+00:00'
assert max_date.isoformat() == '2017-11-14T22:59:00+00:00'
def test_backtest(default_conf, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
exchange._API = Bittrex({'key': '', 'secret': ''})
data = optimize.load_data(ticker_interval=5, pairs=['BTC_ETH'])
results = backtest(default_conf['stake_amount'], optimize.preprocess(data), 10, True)
assert not results.empty
def test_backtest_1min_ticker_interval(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(ticker_interval=1, pairs=['BTC_UNITEST'])
results = backtest(default_conf['stake_amount'], optimize.preprocess(data), 1, True)
assert not results.empty
def trim_dictlist(dl, num):
new = {}
for pair, pair_data in dl.items():
# Can't figure out why -num wont work
new[pair] = pair_data[num:]
return new
def load_data_test(what):
data = optimize.load_data(ticker_interval=1, pairs=['BTC_UNITEST'])
data = trim_dictlist(data, -100)
pair = data['BTC_UNITEST']
datalen = len(pair)
# Depending on the what parameter we now adjust the
# loaded data looks:
# pair :: [{'O': 0.123, 'H': 0.123, 'L': 0.123,
# 'C': 0.123, 'V': 123.123,
# 'T': '2017-11-04T23:02:00', 'BV': 0.123}]
base = 0.001
if what == 'raise':
return {'BTC_UNITEST':
[{'T': pair[x]['T'], # Keep old dates
'V': pair[x]['V'], # Keep old volume
'BV': pair[x]['BV'], # keep too
'O': x * base, # But replace O,H,L,C
'H': x * base + 0.0001,
'L': x * base - 0.0001,
'C': x * base} for x in range(0, datalen)]}
if what == 'lower':
return {'BTC_UNITEST':
[{'T': pair[x]['T'], # Keep old dates
'V': pair[x]['V'], # Keep old volume
'BV': pair[x]['BV'], # keep too
'O': 1 - x * base, # But replace O,H,L,C
'H': 1 - x * base + 0.0001,
'L': 1 - x * base - 0.0001,
'C': 1 - x * base} for x in range(0, datalen)]}
if what == 'sine':
hz = 0.1 # frequency
return {'BTC_UNITEST':
[{'T': pair[x]['T'], # Keep old dates
'V': pair[x]['V'], # Keep old volume
'BV': pair[x]['BV'], # keep too
'O': math.sin(x*hz) / 1000 + base, # But replace O,H,L,C
'H': math.sin(x*hz) / 1000 + base + 0.0001,
'L': math.sin(x*hz) / 1000 + base - 0.0001,
'C': math.sin(x*hz) / 1000 + base} for x in range(0, datalen)]}
return data
def simple_backtest(config, contour, num_results):
data = load_data_test(contour)
processed = optimize.preprocess(data)
assert isinstance(processed, dict)
results = backtest(config['stake_amount'], processed, 1, True)
# results :: <class 'pandas.core.frame.DataFrame'>
assert len(results) == num_results
# Test backtest on offline data
# loaded by freqdata/optimize/__init__.py::load_data()
def test_backtest2(default_conf, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
data = optimize.load_data(ticker_interval=5, pairs=['BTC_ETH'])
results = backtest(default_conf['stake_amount'], optimize.preprocess(data), 10, True)
assert not results.empty
def test_processed(default_conf, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
dict_of_tickerrows = load_data_test('raise')
dataframes = optimize.preprocess(dict_of_tickerrows)
dataframe = dataframes['BTC_UNITEST']
cols = dataframe.columns
# assert the dataframe got some of the indicator columns
for col in ['close', 'high', 'low', 'open', 'date',
'ema50', 'ao', 'macd', 'plus_dm']:
assert col in cols
def test_backtest_pricecontours(default_conf, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
tests = [['raise', 17], ['lower', 0], ['sine', 17]]
for [contour, numres] in tests:
simple_backtest(default_conf, contour, numres)
# Please make this work, the load_config needs to be mocked
# and cleanups.
# def test_backtest_start(default_conf, mocker):
# default_conf['exchange']['pair_whitelist'] = ['BTC_UNITEST']
# mocker.patch.dict('freqtrade.main._CONF', default_conf)
# # see https://pypi.python.org/pypi/pytest-mock/
# # and http://www.voidspace.org.uk/python/mock/patch.html
# # No usage example of simple function mocking,
# # and no documentation of side_effect
# mocker.patch('freqtrade.misc.load_config', new=lambda s, t: {})
# args = MagicMock()
# args.level = 10
# #load_config('foo')
# backtesting.start(args)
#
# Check what sideeffect backtstesting has done.
# Probably need to capture standard-output and
# check for the generated report table.

View File

@ -0,0 +1,79 @@
# pragma pylint: disable=missing-docstring,W0212,C0103
from freqtrade.optimize.hyperopt import calculate_loss, TARGET_TRADES, EXPECTED_MAX_PROFIT, start, \
log_results
def test_loss_calculation_prefer_correct_trade_count():
correct = calculate_loss(1, TARGET_TRADES)
over = calculate_loss(1, TARGET_TRADES + 100)
under = calculate_loss(1, TARGET_TRADES - 100)
assert over > correct
assert under > correct
def test_loss_calculation_has_limited_profit():
correct = calculate_loss(EXPECTED_MAX_PROFIT, TARGET_TRADES)
over = calculate_loss(EXPECTED_MAX_PROFIT * 2, TARGET_TRADES)
under = calculate_loss(EXPECTED_MAX_PROFIT / 2, TARGET_TRADES)
assert over == correct
assert under > correct
def create_trials(mocker):
return mocker.Mock(
results=[{
'loss': 1,
'result': 'foo'
}]
)
def test_start_calls_fmin(mocker):
mocker.patch('freqtrade.optimize.hyperopt.Trials', return_value=create_trials(mocker))
mocker.patch('freqtrade.optimize.preprocess')
mocker.patch('freqtrade.optimize.load_data')
mock_fmin = mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value={})
args = mocker.Mock(epochs=1, config='config.json.example', mongodb=False)
start(args)
mock_fmin.assert_called_once()
def test_start_uses_mongotrials(mocker):
mock_mongotrials = mocker.patch('freqtrade.optimize.hyperopt.MongoTrials',
return_value=create_trials(mocker))
mocker.patch('freqtrade.optimize.preprocess')
mocker.patch('freqtrade.optimize.load_data')
mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value={})
args = mocker.Mock(epochs=1, config='config.json.example', mongodb=True)
start(args)
mock_mongotrials.assert_called_once()
def test_log_results_if_loss_improves(mocker):
logger = mocker.patch('freqtrade.optimize.hyperopt.logger.info')
global CURRENT_BEST_LOSS
CURRENT_BEST_LOSS = 2
log_results({
'loss': 1,
'current_tries': 1,
'total_tries': 2,
'result': 'foo'
})
logger.assert_called_once()
def test_no_log_if_loss_does_not_improve(mocker):
logger = mocker.patch('freqtrade.optimize.hyperopt.logger.info')
global CURRENT_BEST_LOSS
CURRENT_BEST_LOSS = 2
log_results({
'loss': 3,
})
assert not logger.called

View File

@ -0,0 +1,16 @@
# pragma pylint: disable=missing-docstring,W0212
from freqtrade.optimize.hyperopt_conf import hyperopt_optimize_conf
def test_hyperopt_optimize_conf():
hyperopt_conf = hyperopt_optimize_conf()
assert "max_open_trades" in hyperopt_conf
assert "stake_currency" in hyperopt_conf
assert "stake_amount" in hyperopt_conf
assert "minimal_roi" in hyperopt_conf
assert "stoploss" in hyperopt_conf
assert "bid_strategy" in hyperopt_conf
assert "exchange" in hyperopt_conf
assert "pair_whitelist" in hyperopt_conf['exchange']

View File

@ -0,0 +1,166 @@
# pragma pylint: disable=missing-docstring,W0212
import os
import logging
from shutil import copyfile
from freqtrade import exchange, optimize
from freqtrade.exchange import Bittrex
from freqtrade.optimize.__init__ import testdata_path, download_pairs, download_backtesting_testdata
def _backup_file(file: str, copy_file: bool = False) -> None:
"""
Backup existing file to avoid deleting the user file
:param file: complete path to the file
:param touch_file: create an empty file in replacement
:return: None
"""
file_swp = file + '.swp'
if os.path.isfile(file):
os.rename(file, file_swp)
if copy_file:
copyfile(file_swp, file)
def _clean_test_file(file: str) -> None:
"""
Backup existing file to avoid deleting the user file
:param file: complete path to the file
:return: None
"""
file_swp = file + '.swp'
# 1. Delete file from the test
if os.path.isfile(file):
os.remove(file)
# 2. Rollback to the initial file
if os.path.isfile(file_swp):
os.rename(file_swp, file)
def test_load_data_5min_ticker(default_conf, ticker_history, mocker, caplog):
mocker.patch('freqtrade.optimize.get_ticker_history', return_value=ticker_history)
mocker.patch.dict('freqtrade.main._CONF', default_conf)
exchange._API = Bittrex({'key': '', 'secret': ''})
file = 'freqtrade/tests/testdata/BTC_ETH-5.json'
_backup_file(file, copy_file=True)
optimize.load_data(pairs=['BTC_ETH'])
assert os.path.isfile(file) is True
assert ('freqtrade.optimize',
logging.INFO,
'Download the pair: "BTC_ETH", Interval: 5 min'
) not in caplog.record_tuples
_clean_test_file(file)
def test_load_data_1min_ticker(default_conf, ticker_history, mocker, caplog):
mocker.patch('freqtrade.optimize.get_ticker_history', return_value=ticker_history)
mocker.patch.dict('freqtrade.main._CONF', default_conf)
exchange._API = Bittrex({'key': '', 'secret': ''})
file = 'freqtrade/tests/testdata/BTC_ETH-1.json'
_backup_file(file, copy_file=True)
optimize.load_data(ticker_interval=1, pairs=['BTC_ETH'])
assert os.path.isfile(file) is True
assert ('freqtrade.optimize',
logging.INFO,
'Download the pair: "BTC_ETH", Interval: 1 min'
) not in caplog.record_tuples
_clean_test_file(file)
def test_load_data_with_new_pair_1min(default_conf, ticker_history, mocker, caplog):
mocker.patch('freqtrade.optimize.get_ticker_history', return_value=ticker_history)
mocker.patch.dict('freqtrade.main._CONF', default_conf)
exchange._API = Bittrex({'key': '', 'secret': ''})
file = 'freqtrade/tests/testdata/BTC_MEME-1.json'
_backup_file(file)
optimize.load_data(ticker_interval=1, pairs=['BTC_MEME'])
assert os.path.isfile(file) is True
assert ('freqtrade.optimize',
logging.INFO,
'Download the pair: "BTC_MEME", Interval: 1 min'
) in caplog.record_tuples
_clean_test_file(file)
def test_testdata_path():
assert os.path.join('freqtrade', 'tests', 'testdata') in testdata_path()
def test_download_pairs(default_conf, ticker_history, mocker):
mocker.patch('freqtrade.optimize.__init__.get_ticker_history', return_value=ticker_history)
mocker.patch.dict('freqtrade.main._CONF', default_conf)
exchange._API = Bittrex({'key': '', 'secret': ''})
file1_1 = 'freqtrade/tests/testdata/BTC_MEME-1.json'
file1_5 = 'freqtrade/tests/testdata/BTC_MEME-5.json'
file2_1 = 'freqtrade/tests/testdata/BTC_CFI-1.json'
file2_5 = 'freqtrade/tests/testdata/BTC_CFI-5.json'
_backup_file(file1_1)
_backup_file(file1_5)
_backup_file(file2_1)
_backup_file(file2_5)
assert download_pairs(pairs=['BTC-MEME', 'BTC-CFI']) is True
assert os.path.isfile(file1_1) is True
assert os.path.isfile(file1_5) is True
assert os.path.isfile(file2_1) is True
assert os.path.isfile(file2_5) is True
# clean files freshly downloaded
_clean_test_file(file1_1)
_clean_test_file(file1_5)
_clean_test_file(file2_1)
_clean_test_file(file2_5)
def test_download_pairs_exception(default_conf, ticker_history, mocker, caplog):
mocker.patch('freqtrade.optimize.__init__.get_ticker_history', return_value=ticker_history)
mocker.patch('freqtrade.optimize.__init__.download_backtesting_testdata',
side_effect=BaseException('File Error'))
mocker.patch.dict('freqtrade.main._CONF', default_conf)
exchange._API = Bittrex({'key': '', 'secret': ''})
file1_1 = 'freqtrade/tests/testdata/BTC_MEME-1.json'
file1_5 = 'freqtrade/tests/testdata/BTC_MEME-5.json'
_backup_file(file1_1)
_backup_file(file1_5)
download_pairs(pairs=['BTC-MEME'])
# clean files freshly downloaded
_clean_test_file(file1_1)
_clean_test_file(file1_5)
assert ('freqtrade.optimize.__init__',
logging.INFO,
'Failed to download the pair: "BTC-MEME", Interval: 1 min'
) in caplog.record_tuples
def test_download_backtesting_testdata(default_conf, ticker_history, mocker):
mocker.patch('freqtrade.optimize.__init__.get_ticker_history', return_value=ticker_history)
mocker.patch.dict('freqtrade.main._CONF', default_conf)
exchange._API = Bittrex({'key': '', 'secret': ''})
# Download a 1 min ticker file
file1 = 'freqtrade/tests/testdata/BTC_XEL-1.json'
_backup_file(file1)
download_backtesting_testdata(pair="BTC-XEL", interval=1)
assert os.path.isfile(file1) is True
_clean_test_file(file1)
# Download a 5 min ticker file
file2 = 'freqtrade/tests/testdata/BTC_STORJ-5.json'
_backup_file(file2)
download_backtesting_testdata(pair="BTC-STORJ", interval=5)
assert os.path.isfile(file2) is True
_clean_test_file(file2)

View File

@ -1,6 +1,6 @@
# pragma pylint: disable=missing-docstring, too-many-arguments, too-many-ancestors, C0103 # pragma pylint: disable=missing-docstring, too-many-arguments, too-many-ancestors, C0103
import re import re
from datetime import datetime, date from datetime import datetime
from random import randint from random import randint
from unittest.mock import MagicMock from unittest.mock import MagicMock
@ -102,7 +102,7 @@ def test_status_handle(default_conf, update, ticker, mocker):
msg_mock.reset_mock() msg_mock.reset_mock()
# Create some test data # Create some test data
create_trade(15.0) create_trade(0.001)
# Trigger status while we have a fulfilled order for the open trade # Trigger status while we have a fulfilled order for the open trade
_status(bot=MagicMock(), update=update) _status(bot=MagicMock(), update=update)
@ -151,7 +151,8 @@ def test_status_table_handle(default_conf, update, ticker, mocker):
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
def test_profit_handle(default_conf, update, ticker, limit_buy_order, limit_sell_order, mocker): def test_profit_handle(
default_conf, update, ticker, ticker_sell_up, limit_buy_order, limit_sell_order, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True) mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
msg_mock = MagicMock() msg_mock = MagicMock()
@ -163,6 +164,9 @@ def test_profit_handle(default_conf, update, ticker, limit_buy_order, limit_sell
mocker.patch.multiple('freqtrade.main.exchange', mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
get_ticker=ticker) get_ticker=ticker)
mocker.patch.multiple('freqtrade.fiat_convert.Pymarketcap',
ticker=MagicMock(return_value={'price_usd': 15000.0}),
_cache_symbols=MagicMock(return_value={'BTC': 1}))
init(default_conf, create_engine('sqlite://')) init(default_conf, create_engine('sqlite://'))
_profit(bot=MagicMock(), update=update) _profit(bot=MagicMock(), update=update)
@ -171,7 +175,7 @@ def test_profit_handle(default_conf, update, ticker, limit_buy_order, limit_sell
msg_mock.reset_mock() msg_mock.reset_mock()
# Create some test data # Create some test data
create_trade(15.0) create_trade(0.001)
trade = Trade.query.first() trade = Trade.query.first()
# Simulate fulfilled LIMIT_BUY order for trade # Simulate fulfilled LIMIT_BUY order for trade
@ -182,7 +186,10 @@ def test_profit_handle(default_conf, update, ticker, limit_buy_order, limit_sell
assert 'no closed trade' in msg_mock.call_args_list[-1][0][0] assert 'no closed trade' in msg_mock.call_args_list[-1][0][0]
msg_mock.reset_mock() msg_mock.reset_mock()
# Simulate fulfilled LIMIT_SELL order for trade # Update the ticker with a market going up
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker_sell_up)
trade.update(limit_sell_order) trade.update(limit_sell_order)
trade.close_date = datetime.utcnow() trade.close_date = datetime.utcnow()
@ -190,11 +197,17 @@ def test_profit_handle(default_conf, update, ticker, limit_buy_order, limit_sell
_profit(bot=MagicMock(), update=update) _profit(bot=MagicMock(), update=update)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert '*ROI All trades:* `0.00765279 BTC (10.05%)`' in msg_mock.call_args_list[-1][0][0] assert '*ROI:* Close trades' in msg_mock.call_args_list[-1][0][0]
assert 'Best Performing:* `BTC_ETH: 10.05%`' in msg_mock.call_args_list[-1][0][0] assert '∙ `0.00006217 BTC (6.20%)`' in msg_mock.call_args_list[-1][0][0]
assert '∙ `0.933 USD`' in msg_mock.call_args_list[-1][0][0]
assert '*ROI:* All trades' in msg_mock.call_args_list[-1][0][0]
assert '∙ `0.00006217 BTC (6.20%)`' in msg_mock.call_args_list[-1][0][0]
assert '∙ `0.933 USD`' in msg_mock.call_args_list[-1][0][0]
assert '*Best Performing:* `BTC_ETH: 6.20%`' in msg_mock.call_args_list[-1][0][0]
def test_forcesell_handle(default_conf, update, ticker, mocker): def test_forcesell_handle(default_conf, update, ticker, ticker_sell_up, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True) mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock()) rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
@ -205,10 +218,55 @@ def test_forcesell_handle(default_conf, update, ticker, mocker):
mocker.patch.multiple('freqtrade.main.exchange', mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
get_ticker=ticker) get_ticker=ticker)
mocker.patch.multiple('freqtrade.fiat_convert.Pymarketcap',
ticker=MagicMock(return_value={'price_usd': 15000.0}),
_cache_symbols=MagicMock(return_value={'BTC': 1}))
init(default_conf, create_engine('sqlite://')) init(default_conf, create_engine('sqlite://'))
# Create some test data # Create some test data
create_trade(15.0) create_trade(0.001)
trade = Trade.query.first()
assert trade
# Increase the price and sell it
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker_sell_up)
update.message.text = '/forcesell 1'
_forcesell(bot=MagicMock(), update=update)
assert rpc_mock.call_count == 2
assert 'Selling [BTC/ETH]' in rpc_mock.call_args_list[-1][0][0]
assert '0.00001172' in rpc_mock.call_args_list[-1][0][0]
assert 'profit: 6.11%, 0.00006126' in rpc_mock.call_args_list[-1][0][0]
assert '0.919 USD' in rpc_mock.call_args_list[-1][0][0]
def test_forcesell_down_handle(default_conf, update, ticker, ticker_sell_down, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf,
init=MagicMock(),
send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker)
mocker.patch.multiple('freqtrade.fiat_convert.Pymarketcap',
ticker=MagicMock(return_value={'price_usd': 15000.0}),
_cache_symbols=MagicMock(return_value={'BTC': 1}))
init(default_conf, create_engine('sqlite://'))
# Create some test data
create_trade(0.001)
# Decrease the price and sell it
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker_sell_down)
trade = Trade.query.first() trade = Trade.query.first()
assert trade assert trade
@ -218,7 +276,9 @@ def test_forcesell_handle(default_conf, update, ticker, mocker):
assert rpc_mock.call_count == 2 assert rpc_mock.call_count == 2
assert 'Selling [BTC/ETH]' in rpc_mock.call_args_list[-1][0][0] assert 'Selling [BTC/ETH]' in rpc_mock.call_args_list[-1][0][0]
assert '0.07256061 (profit: ~-0.64%)' in rpc_mock.call_args_list[-1][0][0] assert '0.00001044' in rpc_mock.call_args_list[-1][0][0]
assert 'loss: -5.48%, -0.00005492' in rpc_mock.call_args_list[-1][0][0]
assert '-0.824 USD' in rpc_mock.call_args_list[-1][0][0]
def test_exec_forcesell_open_orders(default_conf, ticker, mocker): def test_exec_forcesell_open_orders(default_conf, ticker, mocker):
@ -256,11 +316,14 @@ def test_forcesell_all_handle(default_conf, update, ticker, mocker):
mocker.patch.multiple('freqtrade.main.exchange', mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
get_ticker=ticker) get_ticker=ticker)
mocker.patch.multiple('freqtrade.fiat_convert.Pymarketcap',
ticker=MagicMock(return_value={'price_usd': 15000.0}),
_cache_symbols=MagicMock(return_value={'BTC': 1}))
init(default_conf, create_engine('sqlite://')) init(default_conf, create_engine('sqlite://'))
# Create some test data # Create some test data
for _ in range(4): for _ in range(4):
create_trade(15.0) create_trade(0.001)
rpc_mock.reset_mock() rpc_mock.reset_mock()
update.message.text = '/forcesell all' update.message.text = '/forcesell all'
@ -268,7 +331,9 @@ def test_forcesell_all_handle(default_conf, update, ticker, mocker):
assert rpc_mock.call_count == 4 assert rpc_mock.call_count == 4
for args in rpc_mock.call_args_list: for args in rpc_mock.call_args_list:
assert '0.07256061 (profit: ~-0.64%)' in args[0][0] assert '0.00001098' in args[0][0]
assert 'loss: -0.59%, -0.00000591 BTC' in args[0][0]
assert '-0.089 USD' in args[0][0]
def test_forcesell_handle_invalid(default_conf, update, mocker): def test_forcesell_handle_invalid(default_conf, update, mocker):
@ -323,7 +388,7 @@ def test_performance_handle(
init(default_conf, create_engine('sqlite://')) init(default_conf, create_engine('sqlite://'))
# Create some test data # Create some test data
create_trade(15.0) create_trade(0.001)
trade = Trade.query.first() trade = Trade.query.first()
assert trade assert trade
@ -339,7 +404,7 @@ def test_performance_handle(
_performance(bot=MagicMock(), update=update) _performance(bot=MagicMock(), update=update)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert 'Performance' in msg_mock.call_args_list[0][0][0] assert 'Performance' in msg_mock.call_args_list[0][0][0]
assert '<code>BTC_ETH\t10.05%</code>' in msg_mock.call_args_list[0][0][0] assert '<code>BTC_ETH\t6.20%</code>' in msg_mock.call_args_list[0][0][0]
def test_daily_handle( def test_daily_handle(
@ -355,10 +420,13 @@ def test_daily_handle(
mocker.patch.multiple('freqtrade.main.exchange', mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
get_ticker=ticker) get_ticker=ticker)
mocker.patch.multiple('freqtrade.fiat_convert.Pymarketcap',
ticker=MagicMock(return_value={'price_usd': 15000.0}),
_cache_symbols=MagicMock(return_value={'BTC': 1}))
init(default_conf, create_engine('sqlite://')) init(default_conf, create_engine('sqlite://'))
# Create some test data # Create some test data
create_trade(15.0) create_trade(0.001)
trade = Trade.query.first() trade = Trade.query.first()
assert trade assert trade
@ -371,14 +439,16 @@ def test_daily_handle(
trade.close_date = datetime.utcnow() trade.close_date = datetime.utcnow()
trade.is_open = False trade.is_open = False
# try valid data # Try valid data
update.message.text = '/daily 7' update.message.text = '/daily 2'
_daily(bot=MagicMock(), update=update) _daily(bot=MagicMock(), update=update)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert 'Daily' in msg_mock.call_args_list[0][0][0] assert 'Daily' in msg_mock.call_args_list[0][0][0]
assert str(date.today()) + ' 1.50701325 BTC' in msg_mock.call_args_list[0][0][0] assert str(datetime.utcnow().date()) in msg_mock.call_args_list[0][0][0]
assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0]
assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0]
# try invalid data # Try invalid data
msg_mock.reset_mock() msg_mock.reset_mock()
update_state(State.RUNNING) update_state(State.RUNNING)
update.message.text = '/daily -2' update.message.text = '/daily -2'
@ -409,7 +479,7 @@ def test_count_handle(default_conf, update, ticker, mocker):
update_state(State.RUNNING) update_state(State.RUNNING)
# Create some test data # Create some test data
create_trade(15.0) create_trade(0.001)
msg_mock.reset_mock() msg_mock.reset_mock()
_count(bot=MagicMock(), update=update) _count(bot=MagicMock(), update=update)

View File

@ -0,0 +1,71 @@
from freqtrade.main import refresh_whitelist
# whitelist, blacklist, filtering, all of that will
# eventually become some rules to run on a generic ACL engine
# perhaps try to anticipate that by using some python package
def whitelist_conf():
return {
"stake_currency": "BTC",
"exchange": {
"pair_whitelist": [
"BTC_ETH",
"BTC_TKN",
"BTC_TRST",
"BTC_SWT",
"BTC_BCC"
],
},
}
def get_health():
return [{'Currency': 'ETH',
'IsActive': True
},
{'Currency': 'TKN',
'IsActive': True
}]
def get_health_empty():
return []
# below three test could be merged into a single
# test that ran randomlly generated health lists
def test_refresh_whitelist(mocker):
conf = whitelist_conf()
mocker.patch.dict('freqtrade.main._CONF', conf)
mocker.patch.multiple('freqtrade.main.exchange',
get_wallet_health=get_health)
refreshedwhitelist = refresh_whitelist(conf['exchange']['pair_whitelist'])
whitelist = ['BTC_ETH', 'BTC_TKN']
# Ensure all except those in whitelist are removed
assert set(whitelist) == set(refreshedwhitelist)
def test_refresh_whitelist_dynamic(mocker):
conf = whitelist_conf()
mocker.patch.dict('freqtrade.main._CONF', conf)
mocker.patch.multiple('freqtrade.main.exchange',
get_wallet_health=get_health)
# argument: use the whitelist dynamically by exchange-volume
whitelist = ['BTC_ETH', 'BTC_TKN']
refreshedwhitelist = refresh_whitelist(whitelist)
assert set(whitelist) == set(refreshedwhitelist)
def test_refresh_whitelist_dynamic_empty(mocker):
conf = whitelist_conf()
mocker.patch.dict('freqtrade.main._CONF', conf)
mocker.patch.multiple('freqtrade.main.exchange',
get_wallet_health=get_health_empty)
# argument: use the whitelist dynamically by exchange-volume
whitelist = []
conf['exchange']['pair_whitelist'] = []
refresh_whitelist(whitelist)
pairslist = conf['exchange']['pair_whitelist']
assert set(whitelist) == set(pairslist)

View File

@ -0,0 +1,27 @@
import pandas
from freqtrade import analyze
import freqtrade.optimize
_pairs = ['BTC_ETH']
def load_dataframe_pair(pairs):
ld = freqtrade.optimize.load_data(ticker_interval=5, pairs=pairs)
assert isinstance(ld, dict)
assert isinstance(pairs[0], str)
dataframe = ld[pairs[0]]
dataframe = analyze.analyze_ticker(dataframe)
return dataframe
def test_dataframe_load():
dataframe = load_dataframe_pair(_pairs)
assert isinstance(dataframe, pandas.core.frame.DataFrame)
def test_dataframe_columns_exists():
dataframe = load_dataframe_pair(_pairs)
assert 'high' in dataframe.columns
assert 'low' in dataframe.columns
assert 'close' in dataframe.columns

View File

@ -1,36 +0,0 @@
# pragma pylint: disable=missing-docstring,C0103
from unittest.mock import MagicMock
import pytest
from freqtrade import OperationalException
from freqtrade.exchange import validate_pairs
def test_validate_pairs(default_conf, mocker):
api_mock = MagicMock()
api_mock.get_markets = MagicMock(return_value=[
'BTC_ETH', 'BTC_TKN', 'BTC_TRST', 'BTC_SWT', 'BTC_BCC',
])
mocker.patch('freqtrade.exchange._API', api_mock)
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
validate_pairs(default_conf['exchange']['pair_whitelist'])
def test_validate_pairs_not_available(default_conf, mocker):
api_mock = MagicMock()
api_mock.get_markets = MagicMock(return_value=[])
mocker.patch('freqtrade.exchange._API', api_mock)
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
with pytest.raises(OperationalException, match=r'not available'):
validate_pairs(default_conf['exchange']['pair_whitelist'])
def test_validate_pairs_not_compatible(default_conf, mocker):
api_mock = MagicMock()
api_mock.get_markets = MagicMock(return_value=['BTC_ETH', 'BTC_TKN', 'BTC_TRST', 'BTC_SWT'])
default_conf['stake_currency'] = 'ETH'
mocker.patch('freqtrade.exchange._API', api_mock)
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
with pytest.raises(OperationalException, match=r'not compatible'):
validate_pairs(default_conf['exchange']['pair_whitelist'])

View File

@ -0,0 +1,111 @@
# pragma pylint: disable=missing-docstring, too-many-arguments, too-many-ancestors, C0103
import time
import pytest
from unittest.mock import MagicMock
from freqtrade.fiat_convert import CryptoToFiatConverter, CryptoFiat
def test_pair_convertion_object():
pair_convertion = CryptoFiat(
crypto_symbol='btc',
fiat_symbol='usd',
price=12345.0
)
# Check the cache duration is 6 hours
assert pair_convertion.CACHE_DURATION == 6 * 60 * 60
# Check a regular usage
assert pair_convertion.crypto_symbol == 'BTC'
assert pair_convertion.fiat_symbol == 'USD'
assert pair_convertion.price == 12345.0
assert pair_convertion.is_expired() is False
# Update the expiration time (- 2 hours) and check the behavior
pair_convertion._expiration = time.time() - 2 * 60 * 60
assert pair_convertion.is_expired() is True
# Check set price behaviour
time_reference = time.time() + pair_convertion.CACHE_DURATION
pair_convertion.set_price(price=30000.123)
assert pair_convertion.is_expired() is False
assert pair_convertion._expiration >= time_reference
assert pair_convertion.price == 30000.123
def test_fiat_convert_is_supported():
fiat_convert = CryptoToFiatConverter()
assert fiat_convert._is_supported_fiat(fiat='USD') is True
assert fiat_convert._is_supported_fiat(fiat='usd') is True
assert fiat_convert._is_supported_fiat(fiat='abc') is False
assert fiat_convert._is_supported_fiat(fiat='ABC') is False
def test_fiat_convert_add_pair():
fiat_convert = CryptoToFiatConverter()
assert len(fiat_convert._pairs) == 0
fiat_convert._add_pair(crypto_symbol='btc', fiat_symbol='usd', price=12345.0)
assert len(fiat_convert._pairs) == 1
assert fiat_convert._pairs[0].crypto_symbol == 'BTC'
assert fiat_convert._pairs[0].fiat_symbol == 'USD'
assert fiat_convert._pairs[0].price == 12345.0
fiat_convert._add_pair(crypto_symbol='btc', fiat_symbol='Eur', price=13000.2)
assert len(fiat_convert._pairs) == 2
assert fiat_convert._pairs[1].crypto_symbol == 'BTC'
assert fiat_convert._pairs[1].fiat_symbol == 'EUR'
assert fiat_convert._pairs[1].price == 13000.2
def test_fiat_convert_find_price(mocker):
api_mock = MagicMock(return_value={
'price_usd': 12345.0,
'price_eur': 13000.2
})
mocker.patch('freqtrade.fiat_convert.Pymarketcap.ticker', api_mock)
fiat_convert = CryptoToFiatConverter()
with pytest.raises(ValueError, match=r'The fiat ABC is not supported.'):
fiat_convert._find_price(crypto_symbol='BTC', fiat_symbol='ABC')
assert fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='USD') == 12345.0
assert fiat_convert.get_price(crypto_symbol='btc', fiat_symbol='usd') == 12345.0
assert fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='EUR') == 13000.2
def test_fiat_convert_get_price(mocker):
api_mock = MagicMock(return_value={
'price_usd': 28000.0,
'price_eur': 15000.0
})
mocker.patch('freqtrade.fiat_convert.Pymarketcap.ticker', api_mock)
fiat_convert = CryptoToFiatConverter()
with pytest.raises(ValueError, match=r'The fiat US DOLLAR is not supported.'):
fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='US Dollar')
# Check the value return by the method
assert len(fiat_convert._pairs) == 0
assert fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='USD') == 28000.0
assert fiat_convert._pairs[0].crypto_symbol == 'BTC'
assert fiat_convert._pairs[0].fiat_symbol == 'USD'
assert fiat_convert._pairs[0].price == 28000.0
assert fiat_convert._pairs[0]._expiration is not 0
assert len(fiat_convert._pairs) == 1
# Verify the cached is used
fiat_convert._pairs[0].price = 9867.543
expiration = fiat_convert._pairs[0]._expiration
assert fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='USD') == 9867.543
assert fiat_convert._pairs[0]._expiration == expiration
# Verify the cache expiration
expiration = time.time() - 2 * 60 * 60
fiat_convert._pairs[0]._expiration = expiration
assert fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='USD') == 28000.0
assert fiat_convert._pairs[0]._expiration is not expiration

View File

@ -4,13 +4,14 @@ from unittest.mock import MagicMock
import pytest import pytest
import requests import requests
import logging
from sqlalchemy import create_engine from sqlalchemy import create_engine
from freqtrade import DependencyException, OperationalException from freqtrade import DependencyException, OperationalException
from freqtrade.analyze import SignalType from freqtrade.analyze import SignalType
from freqtrade.exchange import Exchanges from freqtrade.exchange import Exchanges
from freqtrade.main import create_trade, handle_trade, init, \ from freqtrade.main import create_trade, handle_trade, init, \
get_target_bid, _process get_target_bid, _process, execute_sell
from freqtrade.misc import get_state, State from freqtrade.misc import get_state, State
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
@ -40,8 +41,8 @@ def test_process_trade_creation(default_conf, ticker, health, mocker):
assert trade.is_open assert trade.is_open
assert trade.open_date is not None assert trade.open_date is not None
assert trade.exchange == Exchanges.BITTREX.name assert trade.exchange == Exchanges.BITTREX.name
assert trade.open_rate == 0.072661 assert trade.open_rate == 0.00001099
assert trade.amount == 0.6881270557795791 assert trade.amount == 90.99181073703367
def test_process_exchange_failures(default_conf, ticker, health, mocker): def test_process_exchange_failures(default_conf, ticker, health, mocker):
@ -115,11 +116,11 @@ def test_create_trade(default_conf, ticker, limit_buy_order, mocker):
whitelist = copy.deepcopy(default_conf['exchange']['pair_whitelist']) whitelist = copy.deepcopy(default_conf['exchange']['pair_whitelist'])
init(default_conf, create_engine('sqlite://')) init(default_conf, create_engine('sqlite://'))
create_trade(15.0) create_trade(0.001)
trade = Trade.query.first() trade = Trade.query.first()
assert trade is not None assert trade is not None
assert trade.stake_amount == 15.0 assert trade.stake_amount == 0.001
assert trade.is_open assert trade.is_open
assert trade.open_date is not None assert trade.open_date is not None
assert trade.exchange == Exchanges.BITTREX.name assert trade.exchange == Exchanges.BITTREX.name
@ -127,8 +128,8 @@ def test_create_trade(default_conf, ticker, limit_buy_order, mocker):
# Simulate fulfilled LIMIT_BUY order for trade # Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order) trade.update(limit_buy_order)
assert trade.open_rate == 0.07256061 assert trade.open_rate == 0.00001099
assert trade.amount == 206.43811673387373 assert trade.amount == 90.99181073
assert whitelist == default_conf['exchange']['pair_whitelist'] assert whitelist == default_conf['exchange']['pair_whitelist']
@ -179,6 +180,23 @@ def test_create_trade_no_pairs(default_conf, ticker, mocker):
create_trade(default_conf['stake_amount']) create_trade(default_conf['stake_amount'])
def test_create_trade_no_pairs_after_blacklist(default_conf, ticker, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
buy=MagicMock(return_value='mocked_limit_buy'))
with pytest.raises(DependencyException, match=r'.*No pair in whitelist.*'):
conf = copy.deepcopy(default_conf)
conf['exchange']['pair_whitelist'] = ["BTC_ETH"]
conf['exchange']['pair_blacklist'] = ["BTC_ETH"]
mocker.patch.dict('freqtrade.main._CONF', conf)
create_trade(default_conf['stake_amount'])
def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, mocker): def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True) mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
@ -186,14 +204,17 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, mocker):
mocker.patch.multiple('freqtrade.main.exchange', mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
get_ticker=MagicMock(return_value={ get_ticker=MagicMock(return_value={
'bid': 0.17256061, 'bid': 0.00001172,
'ask': 0.172661, 'ask': 0.00001173,
'last': 0.17256061 'last': 0.00001172
}), }),
buy=MagicMock(return_value='mocked_limit_buy'), buy=MagicMock(return_value='mocked_limit_buy'),
sell=MagicMock(return_value='mocked_limit_sell')) sell=MagicMock(return_value='mocked_limit_sell'))
mocker.patch.multiple('freqtrade.fiat_convert.Pymarketcap',
ticker=MagicMock(return_value={'price_usd': 15000.0}),
_cache_symbols=MagicMock(return_value={'BTC': 1}))
init(default_conf, create_engine('sqlite://')) init(default_conf, create_engine('sqlite://'))
create_trade(15.0) create_trade(0.001)
trade = Trade.query.first() trade = Trade.query.first()
assert trade assert trade
@ -207,11 +228,72 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, mocker):
# Simulate fulfilled LIMIT_SELL order for trade # Simulate fulfilled LIMIT_SELL order for trade
trade.update(limit_sell_order) trade.update(limit_sell_order)
assert trade.close_rate == 0.0802134 assert trade.close_rate == 0.00001173
assert trade.close_profit == 0.10046755 assert trade.close_profit == 0.06201057
assert trade.calc_profit() == 0.00006217
assert trade.close_date is not None assert trade.close_date is not None
def test_handle_trade_roi(default_conf, ticker, limit_buy_order, mocker, caplog):
default_conf.update({'experimental': {'use_sell_signal': True}})
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
buy=MagicMock(return_value='mocked_limit_buy'))
mocker.patch('freqtrade.main.min_roi_reached', return_value=True)
init(default_conf, create_engine('sqlite://'))
create_trade(0.001)
trade = Trade.query.first()
trade.is_open = True
# FIX: sniffing logs, suggest handle_trade should not execute_sell
# instead that responsibility should be moved out of handle_trade(),
# we might just want to check if we are in a sell condition without
# executing
# if ROI is reached we must sell
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: False)
assert handle_trade(trade)
assert ('freqtrade', logging.DEBUG, 'Executing sell due to ROI ...') in caplog.record_tuples
# if ROI is reached we must sell even if sell-signal is not signalled
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
assert handle_trade(trade)
assert ('freqtrade', logging.DEBUG, 'Executing sell due to ROI ...') in caplog.record_tuples
def test_handle_trade_experimental(default_conf, ticker, limit_buy_order, mocker, caplog):
default_conf.update({'experimental': {'use_sell_signal': True}})
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
buy=MagicMock(return_value='mocked_limit_buy'))
mocker.patch('freqtrade.main.min_roi_reached', return_value=False)
init(default_conf, create_engine('sqlite://'))
create_trade(0.001)
trade = Trade.query.first()
trade.is_open = True
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: False)
value_returned = handle_trade(trade)
assert ('freqtrade', logging.DEBUG, 'Checking sell_signal ...') in caplog.record_tuples
assert value_returned is False
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
assert handle_trade(trade)
s = 'Executing sell due to sell signal ...'
assert ('freqtrade', logging.DEBUG, s) in caplog.record_tuples
def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, mocker): def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True) mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
@ -223,7 +305,7 @@ def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, mo
# Create trade and sell it # Create trade and sell it
init(default_conf, create_engine('sqlite://')) init(default_conf, create_engine('sqlite://'))
create_trade(15.0) create_trade(0.001)
trade = Trade.query.first() trade = Trade.query.first()
assert trade assert trade
@ -249,3 +331,104 @@ def test_balance_fully_last_side(mocker):
def test_balance_bigger_last_ask(mocker): def test_balance_bigger_last_ask(mocker):
mocker.patch.dict('freqtrade.main._CONF', {'bid_strategy': {'ask_last_balance': 1.0}}) mocker.patch.dict('freqtrade.main._CONF', {'bid_strategy': {'ask_last_balance': 1.0}})
assert get_target_bid({'ask': 5, 'last': 10}) == 5 assert get_target_bid({'ask': 5, 'last': 10}) == 5
def test_execute_sell_up(default_conf, ticker, ticker_sell_up, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
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)
mocker.patch.multiple('freqtrade.fiat_convert.Pymarketcap',
ticker=MagicMock(return_value={'price_usd': 15000.0}),
_cache_symbols=MagicMock(return_value={'BTC': 1}))
init(default_conf, create_engine('sqlite://'))
# Create some test data
create_trade(0.001)
trade = Trade.query.first()
assert trade
# Increase the price and sell it
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker_sell_up)
execute_sell(trade=trade, limit=ticker_sell_up()['bid'])
assert rpc_mock.call_count == 2
assert 'Selling [BTC/ETH]' in rpc_mock.call_args_list[-1][0][0]
assert '0.00001172' in rpc_mock.call_args_list[-1][0][0]
assert 'profit: 6.11%, 0.00006126' in rpc_mock.call_args_list[-1][0][0]
assert '0.919 USD' in rpc_mock.call_args_list[-1][0][0]
def test_execute_sell_down(default_conf, ticker, ticker_sell_down, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
mocker.patch('freqtrade.rpc.init', MagicMock())
rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf,
init=MagicMock(),
send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker)
mocker.patch.multiple('freqtrade.fiat_convert.Pymarketcap',
ticker=MagicMock(return_value={'price_usd': 15000.0}),
_cache_symbols=MagicMock(return_value={'BTC': 1}))
init(default_conf, create_engine('sqlite://'))
# Create some test data
create_trade(0.001)
trade = Trade.query.first()
assert trade
# Decrease the price and sell it
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker_sell_down)
execute_sell(trade=trade, limit=ticker_sell_down()['bid'])
assert rpc_mock.call_count == 2
assert 'Selling [BTC/ETH]' in rpc_mock.call_args_list[-1][0][0]
assert '0.00001044' in rpc_mock.call_args_list[-1][0][0]
assert 'loss: -5.48%, -0.00005492' in rpc_mock.call_args_list[-1][0][0]
assert '-0.824 USD' in rpc_mock.call_args_list[-1][0][0]
def test_execute_sell_without_conf(default_conf, ticker, ticker_sell_up, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
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)
init(default_conf, create_engine('sqlite://'))
# Create some test data
create_trade(0.001)
trade = Trade.query.first()
assert trade
# Increase the price and sell it
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker_sell_up)
mocker.patch('freqtrade.main._CONF', {})
execute_sell(trade=trade, limit=ticker_sell_up()['bid'])
assert rpc_mock.call_count == 2
assert 'Selling [BTC/ETH]' in rpc_mock.call_args_list[-1][0][0]
assert '0.00001172' in rpc_mock.call_args_list[-1][0][0]
assert '(profit: 6.11%, 0.00006126)' in rpc_mock.call_args_list[-1][0][0]
assert 'USD' not in rpc_mock.call_args_list[-1][0][0]

View File

@ -16,16 +16,28 @@ def test_throttle():
return 42 return 42
start = time.time() start = time.time()
result = throttle(func, 0.1) result = throttle(func, min_secs=0.1)
end = time.time() end = time.time()
assert result == 42 assert result == 42
assert end - start > 0.1 assert end - start > 0.1
result = throttle(func, -1) result = throttle(func, min_secs=-1)
assert result == 42 assert result == 42
def test_throttle_with_assets():
def func(nb_assets=-1):
return nb_assets
result = throttle(func, min_secs=0.1, nb_assets=666)
assert result == 666
result = throttle(func, min_secs=0.1)
assert result == -1
def test_parse_args_defaults(): def test_parse_args_defaults():
args = parse_args([]) args = parse_args([])
assert args is not None assert args is not None
@ -73,7 +85,8 @@ def test_parse_args_dynamic_whitelist_invalid_values():
def test_parse_args_backtesting(mocker): def test_parse_args_backtesting(mocker):
backtesting_mock = mocker.patch('freqtrade.optimize.backtesting.start', MagicMock()) backtesting_mock = mocker.patch(
'freqtrade.optimize.backtesting.start', MagicMock())
args = parse_args(['backtesting']) args = parse_args(['backtesting'])
assert args is None assert args is None
assert backtesting_mock.call_count == 1 assert backtesting_mock.call_count == 1
@ -96,7 +109,8 @@ def test_parse_args_backtesting_invalid():
def test_parse_args_backtesting_custom(mocker): def test_parse_args_backtesting_custom(mocker):
backtesting_mock = mocker.patch('freqtrade.optimize.backtesting.start', MagicMock()) backtesting_mock = mocker.patch(
'freqtrade.optimize.backtesting.start', MagicMock())
args = parse_args([ args = parse_args([
'-c', 'test_conf.json', '-c', 'test_conf.json',
'backtesting', 'backtesting',
@ -117,7 +131,8 @@ def test_parse_args_backtesting_custom(mocker):
def test_parse_args_hyperopt(mocker): def test_parse_args_hyperopt(mocker):
hyperopt_mock = mocker.patch('freqtrade.optimize.hyperopt.start', MagicMock()) hyperopt_mock = mocker.patch(
'freqtrade.optimize.hyperopt.start', MagicMock())
args = parse_args(['hyperopt']) args = parse_args(['hyperopt'])
assert args is None assert args is None
assert hyperopt_mock.call_count == 1 assert hyperopt_mock.call_count == 1
@ -130,7 +145,8 @@ def test_parse_args_hyperopt(mocker):
def test_parse_args_hyperopt_custom(mocker): def test_parse_args_hyperopt_custom(mocker):
hyperopt_mock = mocker.patch('freqtrade.optimize.hyperopt.start', MagicMock()) hyperopt_mock = mocker.patch(
'freqtrade.optimize.hyperopt.start', MagicMock())
args = parse_args(['-c', 'test_conf.json', 'hyperopt', '--epochs', '20']) args = parse_args(['-c', 'test_conf.json', 'hyperopt', '--epochs', '20'])
assert args is None assert args is None
assert hyperopt_mock.call_count == 1 assert hyperopt_mock.call_count == 1
@ -155,7 +171,10 @@ def test_load_config(default_conf, mocker):
def test_load_config_invalid_pair(default_conf, mocker): def test_load_config_invalid_pair(default_conf, mocker):
conf = deepcopy(default_conf) conf = deepcopy(default_conf)
conf['exchange']['pair_whitelist'].append('BTC-ETH') conf['exchange']['pair_whitelist'].append('BTC-ETH')
mocker.patch('freqtrade.misc.open', mocker.mock_open(read_data=json.dumps(conf))) mocker.patch(
'freqtrade.misc.open',
mocker.mock_open(
read_data=json.dumps(conf)))
with pytest.raises(ValidationError, match=r'.*does not match.*'): with pytest.raises(ValidationError, match=r'.*does not match.*'):
load_config('somefile') load_config('somefile')
@ -163,6 +182,9 @@ def test_load_config_invalid_pair(default_conf, mocker):
def test_load_config_missing_attributes(default_conf, mocker): def test_load_config_missing_attributes(default_conf, mocker):
conf = deepcopy(default_conf) conf = deepcopy(default_conf)
conf.pop('exchange') conf.pop('exchange')
mocker.patch('freqtrade.misc.open', mocker.mock_open(read_data=json.dumps(conf))) mocker.patch(
'freqtrade.misc.open',
mocker.mock_open(
read_data=json.dumps(conf)))
with pytest.raises(ValidationError, match=r'.*\'exchange\' is a required property.*'): with pytest.raises(ValidationError, match=r'.*\'exchange\' is a required property.*'):
load_config('somefile') load_config('somefile')

View File

@ -1,102 +0,0 @@
# pragma pylint: disable=missing-docstring,W0212
from unittest.mock import MagicMock
from freqtrade import exchange, optimize
from freqtrade.exchange import Bittrex
from freqtrade.optimize.backtesting import backtest
from freqtrade.optimize.__init__ import testdata_path, download_pairs, download_backtesting_testdata
import os
import pytest
def test_backtest(default_conf, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
exchange._API = Bittrex({'key': '', 'secret': ''})
data = optimize.load_data(ticker_interval=5, pairs=['BTC_ETH'])
results = backtest(default_conf, optimize.preprocess(data), 10, True)
num_results = len(results)
assert num_results > 0
def test_1min_ticker_interval(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(ticker_interval=1, pairs=['BTC_UNITEST'])
results = backtest(default_conf, optimize.preprocess(data), 1, True)
assert len(results) > 0
def test_backtest_with_new_pair(default_conf, ticker_history, mocker):
mocker.patch('freqtrade.optimize.get_ticker_history', return_value=ticker_history)
mocker.patch.dict('freqtrade.main._CONF', default_conf)
exchange._API = Bittrex({'key': '', 'secret': ''})
optimize.load_data(ticker_interval=1, pairs=['BTC_MEME'])
file = 'freqtrade/tests/testdata/BTC_MEME-1.json'
assert os.path.isfile(file) is True
# delete file freshly downloaded
if os.path.isfile(file):
os.remove(file)
def test_testdata_path():
assert str('freqtrade/optimize/../tests/testdata') in testdata_path()
def test_download_pairs(default_conf, ticker_history, mocker):
mocker.patch('freqtrade.optimize.__init__.get_ticker_history', return_value=ticker_history)
mocker.patch.dict('freqtrade.main._CONF', default_conf)
exchange._API = Bittrex({'key': '', 'secret': ''})
file1_1 = 'freqtrade/tests/testdata/BTC_MEME-1.json'
file1_5 = 'freqtrade/tests/testdata/BTC_MEME-5.json'
file2_1 = 'freqtrade/tests/testdata/BTC_CFI-1.json'
file2_5 = 'freqtrade/tests/testdata/BTC_CFI-5.json'
assert download_pairs(pairs = ['BTC-MEME', 'BTC-CFI']) is True
assert os.path.isfile(file1_1) is True
assert os.path.isfile(file1_5) is True
assert os.path.isfile(file2_1) is True
assert os.path.isfile(file2_5) is True
# delete files freshly downloaded
if os.path.isfile(file1_1):
os.remove(file1_1)
if os.path.isfile(file1_5):
os.remove(file1_5)
if os.path.isfile(file2_1):
os.remove(file2_1)
if os.path.isfile(file2_5):
os.remove(file2_5)
def test_download_backtesting_testdata(default_conf, ticker_history, mocker):
mocker.patch('freqtrade.optimize.__init__.get_ticker_history', return_value=ticker_history)
mocker.patch.dict('freqtrade.main._CONF', default_conf)
exchange._API = Bittrex({'key': '', 'secret': ''})
# Download a 1 min ticker file
file1 = 'freqtrade/tests/testdata/BTC_XEL-1.json'
download_backtesting_testdata(pair = "BTC-XEL", interval = 1)
assert os.path.isfile(file1) is True
if os.path.isfile(file1):
os.remove(file1)
# Download a 5 min ticker file
file2 = 'freqtrade/tests/testdata/BTC_STORJ-5.json'
download_backtesting_testdata(pair = "BTC-STORJ", interval = 5)
assert os.path.isfile(file2) is True
if os.path.isfile(file2):
os.remove(file2)

View File

@ -1,6 +0,0 @@
# pragma pylint: disable=missing-docstring,W0212
def test_optimizer(default_conf, mocker):
# TODO: implement test
pass

View File

@ -1,15 +1,125 @@
# pragma pylint: disable=missing-docstring # pragma pylint: disable=missing-docstring
import pytest import pytest
import os
from freqtrade.exchange import Exchanges from freqtrade.exchange import Exchanges
from freqtrade.persistence import Trade from freqtrade.persistence import init, Trade
def test_update(limit_buy_order, limit_sell_order): def test_init_create_session(default_conf, mocker):
mocker.patch.dict('freqtrade.persistence._CONF', default_conf)
# Check if init create a session
init(default_conf)
assert hasattr(Trade, 'session')
assert type(Trade.session).__name__ is 'Session'
def test_init_dry_run_db(default_conf, mocker):
default_conf.update({'dry_run_db': True})
mocker.patch.dict('freqtrade.persistence._CONF', default_conf)
# First, protect the existing 'tradesv3.dry_run.sqlite' (Do not delete user data)
dry_run_db = 'tradesv3.dry_run.sqlite'
dry_run_db_swp = dry_run_db + '.swp'
if os.path.isfile(dry_run_db):
os.rename(dry_run_db, dry_run_db_swp)
# Check if the new tradesv3.dry_run.sqlite was created
init(default_conf)
assert os.path.isfile(dry_run_db) is True
# Delete the file made for this unitest and rollback to the previous
# tradesv3.dry_run.sqlite file
# 1. Delete file from the test
if os.path.isfile(dry_run_db):
os.remove(dry_run_db)
# 2. Rollback to the initial file
if os.path.isfile(dry_run_db_swp):
os.rename(dry_run_db_swp, dry_run_db)
def test_init_dry_run_without_db(default_conf, mocker):
default_conf.update({'dry_run_db': False})
mocker.patch.dict('freqtrade.persistence._CONF', default_conf)
# First, protect the existing 'tradesv3.dry_run.sqlite' (Do not delete user data)
dry_run_db = 'tradesv3.dry_run.sqlite'
dry_run_db_swp = dry_run_db + '.swp'
if os.path.isfile(dry_run_db):
os.rename(dry_run_db, dry_run_db_swp)
# Check if the new tradesv3.dry_run.sqlite was created
init(default_conf)
assert os.path.isfile(dry_run_db) is False
# Rollback to the initial 'tradesv3.dry_run.sqlite' file
if os.path.isfile(dry_run_db_swp):
os.rename(dry_run_db_swp, dry_run_db)
def test_init_prod_db(default_conf, mocker):
default_conf.update({'dry_run': False})
mocker.patch.dict('freqtrade.persistence._CONF', default_conf)
# First, protect the existing 'tradesv3.sqlite' (Do not delete user data)
prod_db = 'tradesv3.sqlite'
prod_db_swp = prod_db + '.swp'
if os.path.isfile(prod_db):
os.rename(prod_db, prod_db_swp)
# Check if the new tradesv3.sqlite was created
init(default_conf)
assert os.path.isfile(prod_db) is True
# Delete the file made for this unitest and rollback to the previous tradesv3.sqlite file
# 1. Delete file from the test
if os.path.isfile(prod_db):
os.remove(prod_db)
# Rollback to the initial 'tradesv3.sqlite' file
if os.path.isfile(prod_db_swp):
os.rename(prod_db_swp, prod_db)
def test_update_with_bittrex(limit_buy_order, limit_sell_order):
"""
On this test we will buy and sell a crypto currency.
Buy
- Buy: 90.99181073 Crypto at 0.00001099 BTC
(90.99181073*0.00001099 = 0.0009999 BTC)
- Buying fee: 0.25%
- Total cost of buy trade: 0.001002500 BTC
((90.99181073*0.00001099) + ((90.99181073*0.00001099)*0.0025))
Sell
- Sell: 90.99181073 Crypto at 0.00001173 BTC
(90.99181073*0.00001173 = 0,00106733394 BTC)
- Selling fee: 0.25%
- Total cost of sell trade: 0.001064666 BTC
((90.99181073*0.00001173) - ((90.99181073*0.00001173)*0.0025))
Profit/Loss: +0.000062166 BTC
(Sell:0.001064666 - Buy:0.001002500)
Profit/Loss percentage: 0.0620
((0.001064666/0.001002500)-1 = 6.20%)
:param limit_buy_order:
:param limit_sell_order:
:return:
"""
trade = Trade( trade = Trade(
pair='BTC_ETH', pair='BTC_ETH',
stake_amount=1.00, stake_amount=0.001,
fee=0.1, fee=0.0025,
exchange=Exchanges.BITTREX, exchange=Exchanges.BITTREX,
) )
assert trade.open_order_id is None assert trade.open_order_id is None
@ -20,18 +130,53 @@ def test_update(limit_buy_order, limit_sell_order):
trade.open_order_id = 'something' trade.open_order_id = 'something'
trade.update(limit_buy_order) trade.update(limit_buy_order)
assert trade.open_order_id is None assert trade.open_order_id is None
assert trade.open_rate == 0.07256061 assert trade.open_rate == 0.00001099
assert trade.close_profit is None assert trade.close_profit is None
assert trade.close_date is None assert trade.close_date is None
trade.open_order_id = 'something' trade.open_order_id = 'something'
trade.update(limit_sell_order) trade.update(limit_sell_order)
assert trade.open_order_id is None assert trade.open_order_id is None
assert trade.open_rate == 0.07256061 assert trade.close_rate == 0.00001173
assert trade.close_profit == 0.00546755 assert trade.close_profit == 0.06201057
assert trade.close_date is not None assert trade.close_date is not None
def test_calc_open_close_trade_price(limit_buy_order, limit_sell_order):
trade = Trade(
pair='BTC_ETH',
stake_amount=0.001,
fee=0.0025,
exchange=Exchanges.BITTREX,
)
trade.open_order_id = 'something'
trade.update(limit_buy_order)
assert trade.calc_open_trade_price() == 0.001002500
trade.update(limit_sell_order)
assert trade.calc_close_trade_price() == 0.0010646656
# Profit in BTC
assert trade.calc_profit() == 0.00006217
# Profit in percent
assert trade.calc_profit_percent() == 0.06201057
def test_calc_close_trade_price_exception(limit_buy_order):
trade = Trade(
pair='BTC_ETH',
stake_amount=0.001,
fee=0.0025,
exchange=Exchanges.BITTREX,
)
trade.open_order_id = 'something'
trade.update(limit_buy_order)
assert trade.calc_close_trade_price() == 0.0
def test_update_open_order(limit_buy_order): def test_update_open_order(limit_buy_order):
trade = Trade( trade = Trade(
pair='BTC_ETH', pair='BTC_ETH',
@ -64,3 +209,103 @@ def test_update_invalid_order(limit_buy_order):
limit_buy_order['type'] = 'invalid' limit_buy_order['type'] = 'invalid'
with pytest.raises(ValueError, match=r'Unknown order type'): with pytest.raises(ValueError, match=r'Unknown order type'):
trade.update(limit_buy_order) trade.update(limit_buy_order)
def test_calc_open_trade_price(limit_buy_order):
trade = Trade(
pair='BTC_ETH',
stake_amount=0.001,
fee=0.0025,
exchange=Exchanges.BITTREX,
)
trade.open_order_id = 'open_trade'
trade.update(limit_buy_order) # Buy @ 0.00001099
# Get the open rate price with the standard fee rate
assert trade.calc_open_trade_price() == 0.001002500
# Get the open rate price with a custom fee rate
assert trade.calc_open_trade_price(fee=0.003) == 0.001003000
def test_calc_close_trade_price(limit_buy_order, limit_sell_order):
trade = Trade(
pair='BTC_ETH',
stake_amount=0.001,
fee=0.0025,
exchange=Exchanges.BITTREX,
)
trade.open_order_id = 'close_trade'
trade.update(limit_buy_order) # Buy @ 0.00001099
# Get the close rate price with a custom close rate and a regular fee rate
assert trade.calc_close_trade_price(rate=0.00001234) == 0.0011200318
# Get the close rate price with a custom close rate and a custom fee rate
assert trade.calc_close_trade_price(rate=0.00001234, fee=0.003) == 0.0011194704
# Test when we apply a Sell order, and ask price with a custom fee rate
trade.update(limit_sell_order)
assert trade.calc_close_trade_price(fee=0.005) == 0.0010619972
def test_calc_profit(limit_buy_order, limit_sell_order):
trade = Trade(
pair='BTC_ETH',
stake_amount=0.001,
fee=0.0025,
exchange=Exchanges.BITTREX,
)
trade.open_order_id = 'profit_percent'
trade.update(limit_buy_order) # Buy @ 0.00001099
# Custom closing rate and regular fee rate
# Higher than open rate
assert trade.calc_profit(rate=0.00001234) == 0.00011753
# Lower than open rate
assert trade.calc_profit(rate=0.00000123) == -0.00089086
# Custom closing rate and custom fee rate
# Higher than open rate
assert trade.calc_profit(rate=0.00001234, fee=0.003) == 0.00011697
# Lower than open rate
assert trade.calc_profit(rate=0.00000123, fee=0.003) == -0.00089092
# Only custom fee without sell order applied
with pytest.raises(TypeError):
trade.calc_profit(fee=0.003)
# Test when we apply a Sell order. Sell higher than open rate @ 0.00001173
trade.update(limit_sell_order)
assert trade.calc_profit() == 0.00006217
# Test with a custom fee rate on the close trade
assert trade.calc_profit(fee=0.003) == 0.00006163
def test_calc_profit_percent(limit_buy_order, limit_sell_order):
trade = Trade(
pair='BTC_ETH',
stake_amount=0.001,
fee=0.0025,
exchange=Exchanges.BITTREX,
)
trade.open_order_id = 'profit_percent'
trade.update(limit_buy_order) # Buy @ 0.00001099
# Get percent of profit with a custom rate (Higher than open rate)
assert trade.calc_profit_percent(rate=0.00001234) == 0.1172387
# Get percent of profit with a custom rate (Lower than open rate)
assert trade.calc_profit_percent(rate=0.00000123) == -0.88863827
# Only custom fee without sell order applied
with pytest.raises(TypeError):
trade.calc_profit_percent(fee=0.003)
# Test when we apply a Sell order. Sell higher than open rate @ 0.00001173
trade.update(limit_sell_order)
assert trade.calc_profit_percent() == 0.06201057
# Test with a custom fee rate on the close trade
assert trade.calc_profit_percent(fee=0.003) == 0.0614782

8
install_ta-lib.sh Executable file
View File

@ -0,0 +1,8 @@
if [ ! -f "ta-lib/CHANGELOG.TXT" ]; then
curl -O -L http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz
tar zxvf ta-lib-0.4.0-src.tar.gz
cd ta-lib && ./configure && make && sudo make install && cd ..
else
echo "TA-lib already installed, skipping download and build."
cd ta-lib && sudo make install && cd ..
fi

View File

@ -1,12 +1,12 @@
python-bittrex==0.2.2 python-bittrex==0.2.2
SQLAlchemy==1.1.15 SQLAlchemy==1.2.0
python-telegram-bot==9.0.0 python-telegram-bot==9.0.0
arrow==0.12.0 arrow==0.12.0
cachetools==2.0.1 cachetools==2.0.1
requests==2.18.4 requests==2.18.4
urllib3==1.22 urllib3==1.22
wrapt==1.10.11 wrapt==1.10.11
pandas==0.21.1 pandas==0.22.0
scikit-learn==0.19.1 scikit-learn==0.19.1
scipy==1.0.0 scipy==1.0.0
jsonschema==2.6.0 jsonschema==2.6.0
@ -19,6 +19,7 @@ hyperopt==0.1
# do not upgrade networkx before this is fixed https://github.com/hyperopt/hyperopt/issues/325 # do not upgrade networkx before this is fixed https://github.com/hyperopt/hyperopt/issues/325
networkx==1.11 networkx==1.11
tabulate==0.8.2 tabulate==0.8.2
pymarketcap==3.3.141
# Required for plotting data # Required for plotting data
#matplotlib==2.1.0 #matplotlib==2.1.0

View File

@ -3,7 +3,6 @@
import matplotlib # Install PYQT5 manually if you want to test this helper function import matplotlib # Install PYQT5 manually if you want to test this helper function
matplotlib.use("Qt5Agg") matplotlib.use("Qt5Agg")
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
from freqtrade import exchange, analyze from freqtrade import exchange, analyze
@ -16,7 +15,8 @@ def plot_analyzed_dataframe(pair: str) -> None:
# Init Bittrex to use public API # Init Bittrex to use public API
exchange._API = exchange.Bittrex({'key': '', 'secret': ''}) exchange._API = exchange.Bittrex({'key': '', 'secret': ''})
dataframe = analyze.analyze_ticker(pair) ticker = exchange.get_ticker_history(pair)
dataframe = analyze.analyze_ticker(ticker)
dataframe.loc[dataframe['buy'] == 1, 'buy_price'] = dataframe['close'] dataframe.loc[dataframe['buy'] == 1, 'buy_price'] = dataframe['close']
dataframe.loc[dataframe['sell'] == 1, 'sell_price'] = dataframe['close'] dataframe.loc[dataframe['sell'] == 1, 'sell_price'] = dataframe['close']
@ -51,4 +51,3 @@ def plot_analyzed_dataframe(pair: str) -> None:
if __name__ == '__main__': if __name__ == '__main__':
plot_analyzed_dataframe('BTC_ETH') plot_analyzed_dataframe('BTC_ETH')

View File

@ -35,6 +35,7 @@ setup(name='freqtrade',
'TA-Lib', 'TA-Lib',
'tabulate', 'tabulate',
'cachetools', 'cachetools',
'pymarketcap',
], ],
include_package_data=True, include_package_data=True,
zip_safe=False, zip_safe=False,