Merge branch 'develop' into feature/advanced-status-command

This commit is contained in:
Sébastien Moreau 2017-11-05 10:32:53 -05:00 committed by GitHub
commit 3884cfb809
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 570 additions and 274 deletions

View File

@ -1,2 +1,4 @@
[run] [run]
omit = freqtrade/tests/* omit =
freqtrade/tests/*
freqtrade/vendor/*

View File

@ -1,2 +1,3 @@
[BASIC] [BASIC]
good-names=logger good-names=logger
ignore=vendor

View File

@ -4,6 +4,9 @@ os:
language: python language: python
python: python:
- 3.6 - 3.6
env:
- BACKTEST=
- BACKTEST=true
addons: addons:
apt: apt:
packages: packages:

View File

@ -1,7 +1,5 @@
include LICENSE include LICENSE
include README.md include README.md
include config.json.example include config.json.example
include freqtrade/exchange/*.py recursive-include freqtrade *.py
include freqtrade/rpc/*.py
include freqtrade/tests/*.py
include freqtrade/tests/testdata/*.json include freqtrade/tests/testdata/*.json

View File

@ -16,7 +16,7 @@ and enter the telegram `token` and your `chat_id` in `config.json`
Persistence is achieved through sqlite. Persistence is achieved through sqlite.
#### Telegram RPC commands: ### Telegram RPC commands:
* /start: Starts the trader * /start: Starts the trader
* /stop: Stops the trader * /stop: Stops the trader
* /status [table]: Lists all open trades * /status [table]: Lists all open trades
@ -25,7 +25,7 @@ Persistence is achieved through sqlite.
* /forcesell <trade_id>: Instantly sells the given trade (Ignoring `minimum_roi`). * /forcesell <trade_id>: Instantly sells the given trade (Ignoring `minimum_roi`).
* /performance: Show performance of each finished trade grouped by pair * /performance: Show performance of each finished trade grouped by pair
#### Config ### Config
`minimal_roi` is a JSON object where the key is a duration `minimal_roi` is a JSON object where the key is a duration
in minutes and the value is the minimum ROI in percent. in minutes and the value is the minimum ROI in percent.
See the example below: See the example below:
@ -54,12 +54,18 @@ end up paying more then would probably have been necessary.
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.
#### Prerequisites ### Prerequisites
* 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
#### Install ### Install
#### Arch Linux
Use your favorite AUR helper and install `python-freqtrade-git`.
#### Manually
`master` branch contains the latest stable release. `master` branch contains the latest stable release.
@ -76,18 +82,9 @@ $ pip install -e .
$ ./freqtrade/main.py $ ./freqtrade/main.py
``` ```
There is also an [article](https://www.sales4k.com/blockchain/high-frequency-trading-bot-tutorial/) about how to setup the bot (thanks [@gurghet](https://github.com/gurghet)). There is also an [article](https://www.sales4k.com/blockchain/high-frequency-trading-bot-tutorial/) about how to setup the bot (thanks [@gurghet](https://github.com/gurghet)).*
#### Execute tests \* *Note:* that article was written for an earlier version, so it may be outdated
```
$ pytest
```
This will by default skip the slow running backtest set. To run backtest set:
```
$ BACKTEST=true pytest -s freqtrade/tests/test_backtesting.py
```
#### Docker #### Docker
@ -137,7 +134,18 @@ $ docker start freqtrade
You do not need to rebuild the image for configuration You do not need to rebuild the image for configuration
changes, it will suffice to edit `config.json` and restart the container. changes, it will suffice to edit `config.json` and restart the container.
#### Contributing ### Execute tests
```
$ pytest
```
This will by default skip the slow running backtest set. To run backtest set:
```
$ BACKTEST=true pytest -s freqtrade/tests/test_backtesting.py
```
### Contributing
Feel like our bot is missing a feature? We welcome your pull requests! Few pointers for contributions: Feel like our bot is missing a feature? We welcome your pull requests! Few pointers for contributions:

View File

@ -1,3 +1,3 @@
__version__ = '0.12.0' __version__ = '0.13.0'
from . import main from . import main

View File

@ -5,10 +5,10 @@ from datetime import timedelta
import arrow import arrow
import talib.abstract as ta import talib.abstract as ta
from pandas import DataFrame, to_datetime from pandas import DataFrame, to_datetime
from qtpylib.indicators import awesome_oscillator, crossed_above
from freqtrade import exchange from freqtrade import exchange
from freqtrade.exchange import Bittrex, get_ticker_history from freqtrade.exchange import Bittrex, get_ticker_history
from freqtrade.vendor.qtpylib.indicators import awesome_oscillator
logging.basicConfig(level=logging.DEBUG, logging.basicConfig(level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
@ -17,8 +17,8 @@ logger = logging.getLogger(__name__)
def parse_ticker_dataframe(ticker: list) -> DataFrame: def parse_ticker_dataframe(ticker: list) -> DataFrame:
""" """
Analyses the trend for the given pair Analyses the trend for the given ticker history
:param pair: pair as str in format BTC_ETH or BTC-ETH :param ticker: See exchange.get_ticker_history
:return: DataFrame :return: DataFrame
""" """
df = DataFrame(ticker) \ df = DataFrame(ticker) \
@ -43,8 +43,17 @@ def populate_indicators(dataframe: DataFrame) -> DataFrame:
dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9) dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9)
dataframe['mfi'] = ta.MFI(dataframe) dataframe['mfi'] = ta.MFI(dataframe)
dataframe['cci'] = ta.CCI(dataframe) dataframe['cci'] = ta.CCI(dataframe)
dataframe['rsi'] = ta.RSI(dataframe)
dataframe['mom'] = ta.MOM(dataframe)
dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5)
dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10)
dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50)
dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100)
dataframe['ao'] = awesome_oscillator(dataframe) dataframe['ao'] = awesome_oscillator(dataframe)
macd = ta.MACD(dataframe)
dataframe['macd'] = macd['macd']
dataframe['macdsignal'] = macd['macdsignal']
dataframe['macdhist'] = macd['macdhist']
return dataframe return dataframe
@ -152,7 +161,7 @@ def plot_dataframe(dataframe: DataFrame, pair: str) -> None:
if __name__ == '__main__': if __name__ == '__main__':
# Install PYQT5==5.9 manually if you want to test this helper function # Install PYQT5==5.9 manually if you want to test this helper function
while True: while True:
exchange.EXCHANGE = Bittrex({'key': '', 'secret': ''}) exchange._API = Bittrex({'key': '', 'secret': ''})
test_pair = 'BTC_ETH' test_pair = 'BTC_ETH'
# for pair in ['BTC_ANT', 'BTC_ETH', 'BTC_GNT', 'BTC_ETC']: # for pair in ['BTC_ANT', 'BTC_ETH', 'BTC_GNT', 'BTC_ETC']:
# get_buy_signal(pair) # get_buy_signal(pair)

View File

@ -1,6 +1,6 @@
import enum import enum
import logging import logging
from typing import List from typing import List, Dict
import arrow import arrow
@ -10,7 +10,7 @@ from freqtrade.exchange.interface import Exchange
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Current selected exchange # Current selected exchange
EXCHANGE: Exchange = None _API: Exchange = None
_CONF: dict = {} _CONF: dict = {}
@ -29,7 +29,7 @@ def init(config: dict) -> None:
:param config: config to use :param config: config to use
:return: None :return: None
""" """
global _CONF, EXCHANGE global _CONF, _API
_CONF.update(config) _CONF.update(config)
@ -45,7 +45,7 @@ def init(config: dict) -> None:
except KeyError: except KeyError:
raise RuntimeError('Exchange {} is not supported'.format(name)) raise RuntimeError('Exchange {} is not supported'.format(name))
EXCHANGE = exchange_class(exchange_config) _API = exchange_class(exchange_config)
# Check if all pairs are available # Check if all pairs are available
validate_pairs(config['exchange']['pair_whitelist']) validate_pairs(config['exchange']['pair_whitelist'])
@ -58,58 +58,86 @@ def validate_pairs(pairs: List[str]) -> None:
:param pairs: list of pairs :param pairs: list of pairs
:return: None :return: None
""" """
markets = EXCHANGE.get_markets() markets = _API.get_markets()
for pair in pairs: for pair in pairs:
if pair not in markets: if pair not in markets:
raise RuntimeError('Pair {} is not available at {}'.format(pair, EXCHANGE.name.lower())) raise RuntimeError('Pair {} is not available at {}'.format(pair, _API.name.lower()))
def buy(pair: str, rate: float, amount: float) -> str: def buy(pair: str, rate: float, amount: float) -> str:
if _CONF['dry_run']: if _CONF['dry_run']:
return 'dry_run' return 'dry_run_buy'
return EXCHANGE.buy(pair, rate, amount) return _API.buy(pair, rate, amount)
def sell(pair: str, rate: float, amount: float) -> str: def sell(pair: str, rate: float, amount: float) -> str:
if _CONF['dry_run']: if _CONF['dry_run']:
return 'dry_run' return 'dry_run_sell'
return EXCHANGE.sell(pair, rate, amount) return _API.sell(pair, rate, amount)
def get_balance(currency: str) -> float: def get_balance(currency: str) -> float:
if _CONF['dry_run']: if _CONF['dry_run']:
return 999.9 return 999.9
return EXCHANGE.get_balance(currency) return _API.get_balance(currency)
def get_balances():
if _CONF['dry_run']:
return []
return _API.get_balances()
def get_ticker(pair: str) -> dict: def get_ticker(pair: str) -> dict:
return EXCHANGE.get_ticker(pair) return _API.get_ticker(pair)
def get_ticker_history(pair: str, minimum_date: arrow.Arrow): def get_ticker_history(pair: str, minimum_date: arrow.Arrow):
return EXCHANGE.get_ticker_history(pair, minimum_date) return _API.get_ticker_history(pair, minimum_date)
def cancel_order(order_id: str) -> None: def cancel_order(order_id: str) -> None:
if _CONF['dry_run']: if _CONF['dry_run']:
return return
return EXCHANGE.cancel_order(order_id) return _API.cancel_order(order_id)
def get_open_orders(pair: str) -> List[dict]: def get_order(order_id: str) -> Dict:
if _CONF['dry_run']: if _CONF['dry_run']:
return [] return {
'id': 'dry_run_sell',
'type': 'LIMIT_SELL',
'pair': 'mocked',
'opened': arrow.utcnow().datetime,
'rate': 0.07256060,
'amount': 206.43811673387373,
'remaining': 0.0,
'closed': arrow.utcnow().datetime,
}
return EXCHANGE.get_open_orders(pair) return _API.get_order(order_id)
def get_pair_detail_url(pair: str) -> str: def get_pair_detail_url(pair: str) -> str:
return EXCHANGE.get_pair_detail_url(pair) return _API.get_pair_detail_url(pair)
def get_markets() -> List[str]: def get_markets() -> List[str]:
return EXCHANGE.get_markets() return _API.get_markets()
def get_name() -> str:
return _API.name
def get_sleep_time() -> float:
return _API.sleep_time
def get_fee() -> float:
return _API.fee

View File

@ -1,5 +1,5 @@
import logging import logging
from typing import List, Optional from typing import List, Optional, Dict
import arrow import arrow
import requests import requests
@ -36,6 +36,11 @@ class Bittrex(Exchange):
_EXCHANGE_CONF.update(config) _EXCHANGE_CONF.update(config)
_API = _Bittrex(api_key=_EXCHANGE_CONF['key'], api_secret=_EXCHANGE_CONF['secret']) _API = _Bittrex(api_key=_EXCHANGE_CONF['key'], api_secret=_EXCHANGE_CONF['secret'])
@property
def fee(self) -> float:
# See https://bittrex.com/fees
return 0.0025
def buy(self, pair: str, rate: float, amount: float) -> str: def buy(self, pair: str, rate: float, amount: float) -> str:
data = _API.buy_limit(pair.replace('_', '-'), amount, rate) data = _API.buy_limit(pair.replace('_', '-'), amount, rate)
if not data['success']: if not data['success']:
@ -54,6 +59,12 @@ class Bittrex(Exchange):
raise RuntimeError('{}: {}'.format(self.name.upper(), data['message'])) raise RuntimeError('{}: {}'.format(self.name.upper(), data['message']))
return float(data['result']['Balance'] or 0.0) return float(data['result']['Balance'] or 0.0)
def get_balances(self):
data = _API.get_balances()
if not data['success']:
raise RuntimeError('{}: {}'.format(self.name.upper(), data['message']))
return data['result']
def get_ticker(self, pair: str) -> dict: def get_ticker(self, pair: str) -> dict:
data = _API.get_ticker(pair.replace('_', '-')) data = _API.get_ticker(pair.replace('_', '-'))
if not data['success']: if not data['success']:
@ -81,24 +92,27 @@ class Bittrex(Exchange):
raise RuntimeError('{}: {}'.format(self.name.upper(), data['message'])) raise RuntimeError('{}: {}'.format(self.name.upper(), data['message']))
return data return data
def get_order(self, order_id: str) -> Dict:
data = _API.get_order(order_id)
if not data['success']:
raise RuntimeError('{}: {}'.format(self.name.upper(), data['message']))
data = data['result']
return {
'id': data['OrderUuid'],
'type': data['Type'],
'pair': data['Exchange'].replace('-', '_'),
'opened': data['Opened'],
'rate': data['PricePerUnit'],
'amount': data['Quantity'],
'remaining': data['QuantityRemaining'],
'closed': data['Closed'],
}
def cancel_order(self, order_id: str) -> None: def cancel_order(self, order_id: str) -> None:
data = _API.cancel(order_id) data = _API.cancel(order_id)
if not data['success']: if not data['success']:
raise RuntimeError('{}: {}'.format(self.name.upper(), data['message'])) raise RuntimeError('{}: {}'.format(self.name.upper(), data['message']))
def get_open_orders(self, pair: str) -> List[dict]:
data = _API.get_open_orders(pair.replace('_', '-'))
if not data['success']:
raise RuntimeError('{}: {}'.format(self.name.upper(), data['message']))
return [{
'id': entry['OrderUuid'],
'type': entry['OrderType'],
'opened': entry['Opened'],
'rate': entry['PricePerUnit'],
'amount': entry['Quantity'],
'remaining': entry['QuantityRemaining'],
} for entry in data['result']]
def get_pair_detail_url(self, pair: str) -> str: def get_pair_detail_url(self, pair: str) -> str:
return self.PAIR_DETAIL_METHOD + '?MarketName={}'.format(pair.replace('_', '-')) return self.PAIR_DETAIL_METHOD + '?MarketName={}'.format(pair.replace('_', '-'))

View File

@ -1,5 +1,5 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import List, Optional from typing import List, Optional, Dict
import arrow import arrow
@ -13,6 +13,14 @@ class Exchange(ABC):
""" """
return self.__class__.__name__ return self.__class__.__name__
@property
def fee(self) -> float:
"""
Fee for placing an order
:return: percentage in float
"""
return 0.0
@property @property
@abstractmethod @abstractmethod
def sleep_time(self) -> float: def sleep_time(self) -> float:
@ -49,6 +57,21 @@ class Exchange(ABC):
:return: float :return: float
""" """
@abstractmethod
def get_balances(self) -> List[dict]:
"""
Gets account balances across currencies
:return: List of dicts, format: [
{
'Currency': str,
'Balance': float,
'Available': float,
'Pending': float,
}
...
]
"""
@abstractmethod @abstractmethod
def get_ticker(self, pair: str) -> dict: def get_ticker(self, pair: str) -> dict:
""" """
@ -85,6 +108,22 @@ class Exchange(ABC):
} }
""" """
def get_order(self, order_id: str) -> Dict:
"""
Get order details for the given order_id.
:param order_id: ID as str
:return: dict, format: {
'id': str,
'type': str,
'pair': str,
'opened': str ISO 8601 datetime,
'closed': str ISO 8601 datetime,
'rate': float,
'amount': float,
'remaining': int
}
"""
@abstractmethod @abstractmethod
def cancel_order(self, order_id: str) -> None: def cancel_order(self, order_id: str) -> None:
""" """
@ -93,24 +132,6 @@ class Exchange(ABC):
:return: None :return: None
""" """
@abstractmethod
def get_open_orders(self, pair: str) -> List[dict]:
"""
Gets all open orders for given pair.
:param pair: Pair as str, format: BTC_ETC
:return: List of dicts, format: [
{
'id': str,
'type': str,
'opened': datetime,
'rate': float,
'amount': float,
'remaining': int,
},
...
]
"""
@abstractmethod @abstractmethod
def get_pair_detail_url(self, pair: str) -> str: def get_pair_detail_url(self, pair: str) -> str:
""" """

View File

@ -8,6 +8,7 @@ from datetime import datetime
from typing import Dict, Optional from typing import Dict, Optional
from signal import signal, SIGINT, SIGABRT, SIGTERM from signal import signal, SIGINT, SIGABRT, SIGTERM
import requests
from jsonschema import validate from jsonschema import validate
from freqtrade import __version__, exchange, persistence from freqtrade import __version__, exchange, persistence
@ -44,22 +45,21 @@ def _process() -> None:
logger.exception('Unable to create trade') logger.exception('Unable to create trade')
for trade in trades: for trade in trades:
# Check if there is already an open order for this trade # Get order details for actual price per unit
orders = exchange.get_open_orders(trade.pair) if trade.open_order_id:
orders = [o for o in orders if o['id'] == trade.open_order_id] # Update trade with order values
if orders: logger.info('Got open order for %s', trade)
logger.info('There is an open order for: %s', orders[0]) trade.update(exchange.get_order(trade.open_order_id))
else:
# Update state
trade.open_order_id = None
# Check if this trade can be closed
if not close_trade_if_fulfilled(trade): if not close_trade_if_fulfilled(trade):
# Check if we can sell our current pair # Check if we can sell our current pair
handle_trade(trade) handle_trade(trade)
Trade.session.flush() Trade.session.flush()
except (ConnectionError, json.JSONDecodeError) as error: except (requests.exceptions.ConnectionError, json.JSONDecodeError) as error:
msg = 'Got {} in _process()'.format(error.__class__.__name__) msg = 'Got {} in _process(), retrying in 30 seconds...'.format(error.__class__.__name__)
logger.exception(msg) logger.exception(msg)
time.sleep(30)
def close_trade_if_fulfilled(trade: Trade) -> bool: def close_trade_if_fulfilled(trade: Trade) -> bool:
@ -80,23 +80,25 @@ def close_trade_if_fulfilled(trade: Trade) -> bool:
return False return False
def execute_sell(trade: Trade, current_rate: float) -> None: def execute_sell(trade: Trade, limit: float) -> None:
""" """
Executes a sell for the given trade and current rate Executes a limit sell for the given trade and limit
:param trade: Trade instance :param trade: Trade instance
:param current_rate: current rate :param limit: limit rate for the sell order
:return: None :return: None
""" """
# Get available balance # Execute sell and update trade record
currency = trade.pair.split('_')[1] order_id = exchange.sell(str(trade.pair), limit, trade.amount)
balance = exchange.get_balance(currency) trade.open_order_id = order_id
profit = trade.exec_sell_order(current_rate, balance) trade.close_date = datetime.utcnow()
message = '*{}:* Selling [{}]({}) at rate `{:f} (profit: {}%)`'.format(
fmt_exp_profit = round(trade.calc_profit(limit) * 100, 2)
message = '*{}:* Selling [{}]({}) with limit `{:f} (profit: ~{}%)`'.format(
trade.exchange, trade.exchange,
trade.pair.replace('_', '/'), trade.pair.replace('_', '/'),
exchange.get_pair_detail_url(trade.pair), exchange.get_pair_detail_url(trade.pair),
trade.close_rate, limit,
round(profit, 2) fmt_exp_profit
) )
logger.info(message) logger.info(message)
telegram.send_msg(message) telegram.send_msg(message)
@ -107,17 +109,15 @@ def should_sell(trade: Trade, current_rate: float, current_time: datetime) -> bo
Based an earlier trade and current price and configuration, decides whether bot should sell Based an earlier trade and current price and 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 = (current_rate - trade.open_rate) / trade.open_rate current_profit = trade.calc_profit(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
for duration, threshold in sorted(_CONF['minimal_roi'].items()): for duration, threshold in sorted(_CONF['minimal_roi'].items()):
duration, threshold = float(duration), float(threshold)
# Check if time matches and current rate is above threshold # Check if time matches and current rate is above threshold
time_diff = (current_time - trade.open_date).total_seconds() / 60 time_diff = (current_time - trade.open_date).total_seconds() / 60
if time_diff > 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%%)', current_profit * 100.0)
@ -133,7 +133,7 @@ def handle_trade(trade: Trade) -> None:
if not trade.is_open: if not trade.is_open:
raise ValueError('attempt to handle closed trade: {}'.format(trade)) raise ValueError('attempt to handle closed trade: {}'.format(trade))
logger.debug('Handling open trade %s ...', trade) logger.debug('Handling %s ...', trade)
current_rate = exchange.get_ticker(trade.pair)['bid'] current_rate = exchange.get_ticker(trade.pair)['bid']
if should_sell(trade, current_rate, datetime.utcnow()): if should_sell(trade, current_rate, datetime.utcnow()):
@ -163,7 +163,7 @@ def create_trade(stake_amount: float) -> Optional[Trade]:
# Check if stake_amount is fulfilled # Check if stake_amount is fulfilled
if exchange.get_balance(_CONF['stake_currency']) < stake_amount: if exchange.get_balance(_CONF['stake_currency']) < stake_amount:
raise ValueError( raise ValueError(
'stake amount is not fulfilled (currency={}'.format(_CONF['stake_currency']) 'stake amount is not fulfilled (currency={})'.format(_CONF['stake_currency'])
) )
# Remove currently opened and latest pairs from whitelist # Remove currently opened and latest pairs from whitelist
@ -182,25 +182,29 @@ def create_trade(stake_amount: float) -> Optional[Trade]:
else: else:
return None return None
open_rate = get_target_bid(exchange.get_ticker(pair)) # Calculate amount and subtract fee
amount = stake_amount / open_rate fee = exchange.get_fee()
order_id = exchange.buy(pair, open_rate, amount) buy_limit = get_target_bid(exchange.get_ticker(pair))
amount = (1 - fee) * stake_amount / buy_limit
order_id = exchange.buy(pair, buy_limit, amount)
# Create trade entity and return # Create trade entity and return
message = '*{}:* Buying [{}]({}) at rate `{:f}`'.format( message = '*{}:* Buying [{}]({}) with limit `{:f}`'.format(
exchange.EXCHANGE.name.upper(), exchange.get_name().upper(),
pair.replace('_', '/'), pair.replace('_', '/'),
exchange.get_pair_detail_url(pair), exchange.get_pair_detail_url(pair),
open_rate buy_limit
) )
logger.info(message) logger.info(message)
telegram.send_msg(message) telegram.send_msg(message)
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
return Trade(pair=pair, return Trade(pair=pair,
stake_amount=stake_amount, stake_amount=stake_amount,
open_rate=open_rate,
open_date=datetime.utcnow(),
amount=amount, amount=amount,
exchange=exchange.EXCHANGE.name.upper(), fee=fee * 2,
open_rate=buy_limit,
open_date=datetime.utcnow(),
exchange=exchange.get_name().upper(),
open_order_id=order_id, open_order_id=order_id,
is_open=True) is_open=True)
@ -266,7 +270,7 @@ def app(config: dict) -> None:
elif new_state == State.RUNNING: elif new_state == State.RUNNING:
_process() _process()
# We need to sleep here because otherwise we would run into bittrex rate limit # We need to sleep here because otherwise we would run into bittrex rate limit
time.sleep(exchange.EXCHANGE.sleep_time) time.sleep(exchange.get_sleep_time())
old_state = new_state old_state = new_state
except RuntimeError: except RuntimeError:
telegram.send_msg( telegram.send_msg(

View File

@ -1,16 +1,19 @@
import logging
from datetime import datetime from datetime import datetime
from typing import Optional from decimal import Decimal, getcontext
from typing import Optional, Dict
import arrow
from sqlalchemy import Boolean, Column, DateTime, Float, Integer, String, create_engine from sqlalchemy import Boolean, Column, DateTime, Float, Integer, String, create_engine
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm.scoping import scoped_session from sqlalchemy.orm.scoping import scoped_session
from sqlalchemy.orm.session import sessionmaker from sqlalchemy.orm.session import sessionmaker
from sqlalchemy.types import Enum
from freqtrade import exchange logging.basicConfig(level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
_CONF = {} _CONF = {}
Base = declarative_base() Base = declarative_base()
@ -26,9 +29,9 @@ def init(config: dict, db_url: Optional[str] = None) -> None:
_CONF.update(config) _CONF.update(config)
if not db_url: if not db_url:
if _CONF.get('dry_run', False): if _CONF.get('dry_run', False):
db_url = 'sqlite:///tradesv2.dry_run.sqlite' db_url = 'sqlite:///tradesv3.dry_run.sqlite'
else: else:
db_url = 'sqlite:///tradesv2.sqlite' db_url = 'sqlite:///tradesv3.sqlite'
engine = create_engine(db_url, echo=False) engine = create_engine(db_url, echo=False)
session = scoped_session(sessionmaker(bind=engine, autoflush=True, autocommit=True)) session = scoped_session(sessionmaker(bind=engine, autoflush=True, autocommit=True))
@ -52,44 +55,55 @@ class Trade(Base):
exchange = Column(String, nullable=False) exchange = Column(String, nullable=False)
pair = Column(String, nullable=False) pair = Column(String, nullable=False)
is_open = Column(Boolean, nullable=False, default=True) is_open = Column(Boolean, nullable=False, default=True)
open_rate = Column(Float, nullable=False) fee = Column(Float, nullable=False, default=0.0)
open_rate = Column(Float)
close_rate = Column(Float) close_rate = Column(Float)
close_profit = Column(Float) close_profit = Column(Float)
stake_amount = Column(Float, name='btc_amount', nullable=False) stake_amount = Column(Float, nullable=False)
amount = Column(Float, nullable=False) amount = Column(Float)
open_date = Column(DateTime, nullable=False, default=datetime.utcnow) open_date = Column(DateTime, nullable=False, default=datetime.utcnow)
close_date = Column(DateTime) close_date = Column(DateTime)
open_order_id = Column(String) open_order_id = Column(String)
def __repr__(self): def __repr__(self):
if self.is_open:
open_since = 'closed'
else:
open_since = round((datetime.utcnow() - self.open_date).total_seconds() / 60, 2)
return 'Trade(id={}, pair={}, amount={}, open_rate={}, open_since={})'.format( return 'Trade(id={}, pair={}, amount={}, open_rate={}, open_since={})'.format(
self.id, self.id,
self.pair, self.pair,
self.amount, self.amount,
self.open_rate, self.open_rate,
open_since arrow.get(self.open_date).humanize() if self.is_open else 'closed'
) )
def exec_sell_order(self, rate: float, amount: float) -> float: def update(self, order: Dict) -> None:
""" """
Executes a sell for the given trade and updated the entity. Updates this entity with amount and actual open/close rates.
:param rate: rate to sell for :param order: order retrieved by exchange.get_order()
:param amount: amount to sell :return: None
:return: current profit as percentage
""" """
profit = 100 * ((rate - self.open_rate) / self.open_rate) if not order['closed']:
return
# Execute sell and update trade record logger.debug('Updating trade (id=%d) ...', self.id)
order_id = exchange.sell(str(self.pair), rate, amount) if order['type'] == 'LIMIT_BUY':
self.close_rate = rate # Update open rate and actual amount
self.close_profit = profit self.open_rate = order['rate']
self.close_date = datetime.utcnow() self.amount = order['amount']
self.open_order_id = order_id elif order['type'] == 'LIMIT_SELL':
# Set close rate and set actual profit
self.close_rate = order['rate']
self.close_profit = self.calc_profit()
else:
raise ValueError('Unknown order type: {}'.format(order['type']))
# Flush changes self.open_order_id = None
Trade.session.flush()
return profit def calc_profit(self, rate: Optional[float] = None) -> float:
"""
Calculates the profit in percentage (including fee).
:param rate: rate to compare with (optional).
If rate is not set self.close_rate will be used
:return: profit in percentage as float
"""
getcontext().prec = 8
return float((Decimal(rate or self.close_rate) - Decimal(self.open_rate))
/ Decimal(self.open_rate) - Decimal(self.fee))

View File

@ -35,7 +35,7 @@ def init(config: dict) -> None:
global _updater global _updater
_CONF.update(config) _CONF.update(config)
if not _CONF['telegram']['enabled']: if not is_enabled():
return return
_updater = Updater(token=config['telegram']['token'], workers=0) _updater = Updater(token=config['telegram']['token'], workers=0)
@ -44,6 +44,7 @@ def init(config: dict) -> None:
handles = [ handles = [
CommandHandler('status', _status), CommandHandler('status', _status),
CommandHandler('profit', _profit), CommandHandler('profit', _profit),
CommandHandler('balance', _balance),
CommandHandler('start', _start), CommandHandler('start', _start),
CommandHandler('stop', _stop), CommandHandler('stop', _stop),
CommandHandler('forcesell', _forcesell), CommandHandler('forcesell', _forcesell),
@ -70,9 +71,18 @@ def cleanup() -> None:
Stops all running telegram threads. Stops all running telegram threads.
:return: None :return: None
""" """
if not is_enabled():
return
_updater.stop() _updater.stop()
def is_enabled() -> bool:
"""
Returns True if the telegram module is activated, False otherwise
"""
return bool(_CONF['telegram'].get('enabled', False))
def authorized_only(command_handler: Callable[[Bot, Update], None]) -> Callable[..., Any]: def authorized_only(command_handler: Callable[[Bot, Update], None]) -> Callable[..., Any]:
""" """
Decorator to check if the message comes from the correct chat_id Decorator to check if the message comes from the correct chat_id
@ -116,18 +126,15 @@ def _status(bot: Bot, update: Update) -> None:
if get_state() != State.RUNNING: if get_state() != State.RUNNING:
send_msg('*Status:* `trader is not running`', bot=bot) send_msg('*Status:* `trader is not running`', bot=bot)
elif not trades: elif not trades:
send_msg('*Status:* `no active order`', bot=bot) send_msg('*Status:* `no active trade`', bot=bot)
else: else:
for trade in trades: for trade in trades:
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 = 100 * ((current_rate - trade.open_rate) / trade.open_rate) current_profit = trade.calc_profit(current_rate)
orders = exchange.get_open_orders(trade.pair)
orders = [o for o in orders if o['id'] == trade.open_order_id]
order = orders[0] if orders else None
fmt_close_profit = '{:.2f}%'.format( fmt_close_profit = '{:.2f}%'.format(
round(trade.close_profit, 2) round(trade.close_profit * 100, 2)
) if trade.close_profit else None ) if trade.close_profit else None
message = """ message = """
*Trade ID:* `{trade_id}` *Trade ID:* `{trade_id}`
@ -150,8 +157,10 @@ def _status(bot: Bot, update: Update) -> None:
current_rate=current_rate, current_rate=current_rate,
amount=round(trade.amount, 8), amount=round(trade.amount, 8),
close_profit=fmt_close_profit, close_profit=fmt_close_profit,
current_profit=round(current_profit, 2), current_profit=round(current_profit * 100, 2),
open_order='{} ({})'.format(order['remaining'], order['type']) if order else None, open_order='{} ({})'.format(
order['remaining'], order['type']
) if order else None,
) )
send_msg(message, bot=bot) send_msg(message, bot=bot)
@ -214,6 +223,8 @@ def _profit(bot: Bot, update: Update) -> None:
profits = [] profits = []
durations = [] durations = []
for trade in trades: for trade in trades:
if not trade.open_rate:
continue
if trade.close_date: if trade.close_date:
durations.append((trade.close_date - trade.open_date).total_seconds()) durations.append((trade.close_date - trade.open_date).total_seconds())
if trade.close_profit: if trade.close_profit:
@ -221,9 +232,9 @@ def _profit(bot: Bot, update: Update) -> None:
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 = 100 * ((current_rate - trade.open_rate) / trade.open_rate) profit = trade.calc_profit(current_rate)
profit_amounts.append((profit / 100) * trade.stake_amount) profit_amounts.append(profit * trade.stake_amount)
profits.append(profit) profits.append(profit)
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')) \
@ -238,25 +249,49 @@ def _profit(bot: Bot, update: Update) -> None:
bp_pair, bp_rate = best_pair bp_pair, bp_rate = best_pair
markdown_msg = """ markdown_msg = """
*ROI:* `{profit_btc:.2f} ({profit:.2f}%)` *ROI:* `{profit_btc:.6f} ({profit:.2f}%)`
*Trade Count:* `{trade_count}` *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}%`
{dry_run_info}
""".format( """.format(
profit_btc=round(sum(profit_amounts), 8), profit_btc=round(sum(profit_amounts), 8),
profit=round(sum(profits), 2), profit=round(sum(profits) * 100, 2),
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(),
avg_duration=str(timedelta(seconds=sum(durations) / float(len(durations)))).split('.')[0], avg_duration=str(timedelta(seconds=sum(durations) / float(len(durations)))).split('.')[0],
best_pair=bp_pair, best_pair=bp_pair,
best_rate=round(bp_rate, 2), best_rate=round(bp_rate * 100, 2),
dry_run_info='\n*NOTE:* These values are mocked because *dry_run* is enabled!'
if _CONF['dry_run'] else ''
) )
send_msg(markdown_msg, bot=bot) send_msg(markdown_msg, bot=bot)
@authorized_only
def _balance(bot: Bot, update: Update) -> None:
"""
Handler for /balance
Returns current account balance per crypto
"""
output = ""
balances = exchange.get_balances()
for currency in balances:
if not currency['Balance'] and not currency['Available'] and not currency['Pending']:
continue
output += """*Currency*: {Currency}
*Available*: {Available}
*Balance*: {Balance}
*Pending*: {Pending}
""".format(**currency)
send_msg(output)
@authorized_only @authorized_only
def _start(bot: Bot, update: Update) -> None: def _start(bot: Bot, update: Update) -> None:
""" """
@ -315,20 +350,8 @@ def _forcesell(bot: Bot, update: Update) -> None:
return return
# Get current rate # Get current rate
current_rate = exchange.get_ticker(trade.pair)['bid'] current_rate = exchange.get_ticker(trade.pair)['bid']
# Get available balance from freqtrade.main import execute_sell
currency = trade.pair.split('_')[1] execute_sell(trade, current_rate)
balance = exchange.get_balance(currency)
# Execute sell
profit = trade.exec_sell_order(current_rate, balance)
message = '*{}:* Selling [{}]({}) at rate `{:f} (profit: {}%)`'.format(
trade.exchange,
trade.pair.replace('_', '/'),
exchange.get_pair_detail_url(trade.pair),
trade.close_rate,
round(profit, 2)
)
logger.info(message)
send_msg(message)
except ValueError: except ValueError:
send_msg('Invalid argument. Usage: `/forcesell <trade_id>`') send_msg('Invalid argument. Usage: `/forcesell <trade_id>`')
@ -357,10 +380,14 @@ def _performance(bot: Bot, update: Update) -> None:
stats = '\n'.join('{index}. <code>{pair}\t{profit:.2f}%</code>'.format( stats = '\n'.join('{index}. <code>{pair}\t{profit:.2f}%</code>'.format(
index=i + 1, index=i + 1,
pair=pair, pair=pair,
profit=round(rate, 2) profit=round(rate * 100, 2)
) for i, (pair, rate) in enumerate(pair_rates)) ) for i, (pair, rate) in enumerate(pair_rates))
message = '<b>Performance:</b>\n{}\n'.format(stats) message = '<b>Performance:</b>\n{}\n{}'.format(
stats,
'<b>NOTE:</b> These values are mocked because <b>dry_run</b> is enabled.'
if _CONF['dry_run'] else ''
)
logger.debug(message) logger.debug(message)
send_msg(message, parse_mode=ParseMode.HTML) send_msg(message, parse_mode=ParseMode.HTML)
@ -403,6 +430,7 @@ def _help(bot: Bot, update: Update) -> None:
*/forcesell <trade_id>:* `Instantly sells the given trade, regardless of profit` */forcesell <trade_id>:* `Instantly sells the given trade, regardless of profit`
*/performance:* `Show performance of each finished trade grouped by pair` */performance:* `Show performance of each finished trade grouped by pair`
*/count:* `Show number of trades running compared to allowed number of trades` */count:* `Show number of trades running compared to allowed number of trades`
*/balance:* `Show account balance per currency`
*/help:* `This help message` */help:* `This help message`
""" """
send_msg(message, bot=bot) send_msg(message, bot=bot)
@ -428,7 +456,8 @@ def send_msg(msg: str, bot: Bot = None, parse_mode: ParseMode = ParseMode.MARKDO
:param parse_mode: telegram parse mode :param parse_mode: telegram parse mode
:return: None :return: None
""" """
if _CONF['telegram'].get('enabled', False): if not is_enabled():
return
try: try:
bot = bot or _updater.bot bot = bot or _updater.bot
try: try:

View File

@ -7,6 +7,7 @@ from pandas import DataFrame
from freqtrade.analyze import parse_ticker_dataframe, populate_buy_trend, populate_indicators, \ from freqtrade.analyze import parse_ticker_dataframe, populate_buy_trend, populate_indicators, \
get_buy_signal get_buy_signal
@pytest.fixture @pytest.fixture
def result(): def result():
with open('freqtrade/tests/testdata/btc-eth.json') as data_file: with open('freqtrade/tests/testdata/btc-eth.json') as data_file:
@ -14,18 +15,22 @@ def result():
return parse_ticker_dataframe(data['result']) return parse_ticker_dataframe(data['result'])
def test_dataframe_has_correct_columns(result): def test_dataframe_has_correct_columns(result):
assert result.columns.tolist() == \ assert result.columns.tolist() == \
['close', 'high', 'low', 'open', 'date', 'volume'] ['close', 'high', 'low', 'open', 'date', 'volume']
def test_dataframe_has_correct_length(result): def test_dataframe_has_correct_length(result):
assert len(result.index) == 5751 assert len(result.index) == 5751
def test_populates_buy_trend(result): def test_populates_buy_trend(result):
dataframe = populate_buy_trend(populate_indicators(result)) dataframe = populate_buy_trend(populate_indicators(result))
assert 'buy' in dataframe.columns assert 'buy' in dataframe.columns
assert 'buy_price' in dataframe.columns assert 'buy_price' in dataframe.columns
def test_returns_latest_buy_signal(mocker): def test_returns_latest_buy_signal(mocker):
buydf = DataFrame([{'buy': 1, 'date': datetime.today()}]) buydf = DataFrame([{'buy': 1, 'date': datetime.today()}])
mocker.patch('freqtrade.analyze.analyze_ticker', return_value=buydf) mocker.patch('freqtrade.analyze.analyze_ticker', return_value=buydf)

View File

@ -7,12 +7,15 @@ import pytest
import arrow import arrow
from pandas import DataFrame from pandas import DataFrame
from freqtrade import exchange
from freqtrade.analyze import analyze_ticker from freqtrade.analyze import analyze_ticker
from freqtrade.exchange import Bittrex
from freqtrade.main import should_sell from freqtrade.main import should_sell
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
logging.disable(logging.DEBUG) # disable debug logs that slow backtesting a lot logging.disable(logging.DEBUG) # disable debug logs that slow backtesting a lot
def format_results(results): def format_results(results):
return 'Made {} buys. Average profit {:.2f}%. Total profit was {:.3f}. Average duration {:.1f} mins.'.format( return 'Made {} buys. Average profit {:.2f}%. Total profit was {:.3f}. Average duration {:.1f} mins.'.format(
len(results.index), len(results.index),
@ -21,15 +24,18 @@ def format_results(results):
results.duration.mean() * 5 results.duration.mean() * 5
) )
def print_pair_results(pair, results): def print_pair_results(pair, results):
print('For currency {}:'.format(pair)) print('For currency {}:'.format(pair))
print(format_results(results[results.currency == pair])) print(format_results(results[results.currency == pair]))
@pytest.fixture @pytest.fixture
def pairs(): def pairs():
return ['btc-neo', 'btc-eth', 'btc-omg', 'btc-edg', 'btc-pay', return ['btc-neo', 'btc-eth', 'btc-omg', 'btc-edg', 'btc-pay',
'btc-pivx', 'btc-qtum', 'btc-mtl', 'btc-etc', 'btc-ltc'] 'btc-pivx', 'btc-qtum', 'btc-mtl', 'btc-etc', 'btc-ltc']
@pytest.fixture @pytest.fixture
def conf(): def conf():
return { return {
@ -42,23 +48,29 @@ def conf():
"stoploss": -0.40 "stoploss": -0.40
} }
def backtest(conf, pairs, mocker): def backtest(conf, pairs, mocker):
trades = [] trades = []
exchange._API = Bittrex({'key': '', 'secret': ''})
mocked_history = mocker.patch('freqtrade.analyze.get_ticker_history')
mocker.patch.dict('freqtrade.main._CONF', conf) mocker.patch.dict('freqtrade.main._CONF', conf)
mocker.patch('arrow.utcnow', return_value=arrow.get('2017-08-20T14:50:00'))
for pair in pairs: for pair in pairs:
with open('freqtrade/tests/testdata/'+pair+'.json') as data_file: with open('freqtrade/tests/testdata/'+pair+'.json') as data_file:
data = json.load(data_file) mocked_history.return_value = json.load(data_file)
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=data)
mocker.patch('arrow.utcnow', return_value=arrow.get('2017-08-20T14:50:00'))
ticker = analyze_ticker(pair)[['close', 'date', 'buy']].copy() ticker = analyze_ticker(pair)[['close', 'date', 'buy']].copy()
# for each buy point # for each buy point
for row in ticker[ticker.buy == 1].itertuples(index=True): for row in ticker[ticker.buy == 1].itertuples(index=True):
trade = Trade(open_rate=row.close, open_date=row.date, amount=1) trade = Trade(
open_rate=row.close,
open_date=row.date,
amount=1,
fee=exchange.get_fee()*2
)
# calculate win/lose forwards from buy point # calculate win/lose forwards from buy point
for row2 in ticker[row.Index:].itertuples(index=True): for row2 in ticker[row.Index:].itertuples(index=True):
if should_sell(trade, row2.close, row2.date): if should_sell(trade, row2.close, row2.date):
current_profit = (row2.close - trade.open_rate) / trade.open_rate current_profit = trade.calc_profit(row2.close)
trades.append((pair, current_profit, row2.Index - row.Index)) trades.append((pair, current_profit, row2.Index - row.Index))
break break
@ -66,11 +78,13 @@ def backtest(conf, pairs, mocker):
results = DataFrame.from_records(trades, columns=labels) results = DataFrame.from_records(trades, columns=labels)
return results return results
@pytest.mark.skipif(not os.environ.get('BACKTEST', False), reason="BACKTEST not set") @pytest.mark.skipif(not os.environ.get('BACKTEST', False), reason="BACKTEST not set")
def test_backtest(conf, pairs, mocker, report=True): def test_backtest(conf, pairs, mocker, report=True):
results = backtest(conf, pairs, mocker) results = backtest(conf, pairs, mocker)
print('====================== BACKTESTING REPORT ================================') print('====================== BACKTESTING REPORT ================================')
[print_pair_results(pair, results) for pair in pairs] for pair in pairs:
print_pair_results(pair, results)
print('TOTAL OVER ALL TRADES:') print('TOTAL OVER ALL TRADES:')
print(format_results(results)) print(format_results(results))

View File

@ -1,16 +1,16 @@
# pragma pylint: disable=missing-docstring # pragma pylint: disable=missing-docstring
from operator import itemgetter
import logging import logging
import os import os
from functools import reduce from functools import reduce
from math import exp from math import exp
import pytest from operator import itemgetter
from pandas import DataFrame
from qtpylib.indicators import crossed_above
import pytest
from hyperopt import fmin, tpe, hp, Trials, STATUS_OK from hyperopt import fmin, tpe, hp, Trials, STATUS_OK
from pandas import DataFrame
from freqtrade.tests.test_backtesting import backtest, format_results from freqtrade.tests.test_backtesting import backtest, format_results
from freqtrade.vendor.qtpylib.indicators import crossed_above
logging.disable(logging.DEBUG) # disable debug logs that slow backtesting a lot logging.disable(logging.DEBUG) # disable debug logs that slow backtesting a lot
@ -23,6 +23,7 @@ def pairs():
return ['btc-neo', 'btc-eth', 'btc-omg', 'btc-edg', 'btc-pay', return ['btc-neo', 'btc-eth', 'btc-omg', 'btc-edg', 'btc-pay',
'btc-pivx', 'btc-qtum', 'btc-mtl', 'btc-etc', 'btc-ltc'] 'btc-pivx', 'btc-qtum', 'btc-mtl', 'btc-etc', 'btc-ltc']
@pytest.fixture @pytest.fixture
def conf(): def conf():
return { return {
@ -35,15 +36,15 @@ def conf():
"stoploss": -0.05 "stoploss": -0.05
} }
def buy_strategy_generator(params): def buy_strategy_generator(params):
print(params) print(params)
def populate_buy_trend(dataframe: DataFrame) -> DataFrame: def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
conditions = [] conditions = []
# GUARDS AND TRENDS # GUARDS AND TRENDS
if params['below_sma']['enabled']: if params['uptrend_long_ema']['enabled']:
conditions.append(dataframe['close'] < dataframe['sma']) conditions.append(dataframe['ema50'] > dataframe['ema100'])
if params['over_sma']['enabled']:
conditions.append(dataframe['close'] > dataframe['sma'])
if params['mfi']['enabled']: if params['mfi']['enabled']:
conditions.append(dataframe['mfi'] < params['mfi']['value']) conditions.append(dataframe['mfi'] < params['mfi']['value'])
if params['fastd']['enabled']: if params['fastd']['enabled']:
@ -52,6 +53,8 @@ def buy_strategy_generator(params):
conditions.append(dataframe['adx'] > params['adx']['value']) conditions.append(dataframe['adx'] > params['adx']['value'])
if params['cci']['enabled']: if params['cci']['enabled']:
conditions.append(dataframe['cci'] < params['cci']['value']) conditions.append(dataframe['cci'] < params['cci']['value'])
if params['rsi']['enabled']:
conditions.append(dataframe['rsi'] < params['rsi']['value'])
if params['over_sar']['enabled']: if params['over_sar']['enabled']:
conditions.append(dataframe['close'] > dataframe['sar']) conditions.append(dataframe['close'] > dataframe['sar'])
if params['uptrend_sma']['enabled']: if params['uptrend_sma']['enabled']:
@ -64,6 +67,8 @@ def buy_strategy_generator(params):
'lower_bb': dataframe['tema'] <= dataframe['blower'], 'lower_bb': dataframe['tema'] <= dataframe['blower'],
'faststoch10': (dataframe['fastd'] >= 10) & (prev_fastd < 10), 'faststoch10': (dataframe['fastd'] >= 10) & (prev_fastd < 10),
'ao_cross_zero': (crossed_above(dataframe['ao'], 0.0)), 'ao_cross_zero': (crossed_above(dataframe['ao'], 0.0)),
'ema5_cross_ema10': (crossed_above(dataframe['ema5'], dataframe['ema10'])),
'macd_cross_signal': (crossed_above(dataframe['macd'], dataframe['macdsignal'])),
} }
conditions.append(triggers.get(params['trigger']['type'])) conditions.append(triggers.get(params['trigger']['type']))
@ -75,11 +80,14 @@ def buy_strategy_generator(params):
return dataframe return dataframe
return populate_buy_trend return populate_buy_trend
@pytest.mark.skipif(not os.environ.get('BACKTEST', False), reason="BACKTEST not set") @pytest.mark.skipif(not os.environ.get('BACKTEST', False), reason="BACKTEST not set")
def test_hyperopt(conf, pairs, mocker): def test_hyperopt(conf, pairs, mocker):
mocked_buy_trend = mocker.patch('freqtrade.analyze.populate_buy_trend')
def optimizer(params): def optimizer(params):
buy_strategy = buy_strategy_generator(params) mocked_buy_trend.side_effect = buy_strategy_generator(params)
mocker.patch('freqtrade.analyze.populate_buy_trend', side_effect=buy_strategy)
results = backtest(conf, pairs, mocker) results = backtest(conf, pairs, mocker)
result = format_results(results) result = format_results(results)
@ -100,25 +108,25 @@ def test_hyperopt(conf, pairs, mocker):
space = { space = {
'mfi': hp.choice('mfi', [ 'mfi': hp.choice('mfi', [
{'enabled': False}, {'enabled': False},
{'enabled': True, 'value': hp.uniform('mfi-value', 2, 40)} {'enabled': True, 'value': hp.uniform('mfi-value', 5, 15)}
]), ]),
'fastd': hp.choice('fastd', [ 'fastd': hp.choice('fastd', [
{'enabled': False}, {'enabled': False},
{'enabled': True, 'value': hp.uniform('fastd-value', 2, 40)} {'enabled': True, 'value': hp.uniform('fastd-value', 5, 40)}
]), ]),
'adx': hp.choice('adx', [ 'adx': hp.choice('adx', [
{'enabled': False}, {'enabled': False},
{'enabled': True, 'value': hp.uniform('adx-value', 2, 40)} {'enabled': True, 'value': hp.uniform('adx-value', 10, 30)}
]), ]),
'cci': hp.choice('cci', [ 'cci': hp.choice('cci', [
{'enabled': False}, {'enabled': False},
{'enabled': True, 'value': hp.uniform('cci-value', -200, -100)} {'enabled': True, 'value': hp.uniform('cci-value', -150, -100)}
]), ]),
'below_sma': hp.choice('below_sma', [ 'rsi': hp.choice('rsi', [
{'enabled': False}, {'enabled': False},
{'enabled': True} {'enabled': True, 'value': hp.uniform('rsi-value', 20, 30)}
]), ]),
'over_sma': hp.choice('over_sma', [ 'uptrend_long_ema': hp.choice('uptrend_long_ema', [
{'enabled': False}, {'enabled': False},
{'enabled': True} {'enabled': True}
]), ]),
@ -133,11 +141,13 @@ def test_hyperopt(conf, pairs, mocker):
'trigger': hp.choice('trigger', [ 'trigger': hp.choice('trigger', [
{'type': 'lower_bb'}, {'type': 'lower_bb'},
{'type': 'faststoch10'}, {'type': 'faststoch10'},
{'type': 'ao_cross_zero'} {'type': 'ao_cross_zero'},
{'type': 'ema5_cross_ema10'},
{'type': 'macd_cross_signal'},
]), ]),
} }
trials = Trials() trials = Trials()
best = fmin(fn=optimizer, space=space, algo=tpe.suggest, max_evals=40, trials=trials) best = fmin(fn=optimizer, space=space, algo=tpe.suggest, max_evals=4, trials=trials)
print('\n\n\n\n====================== HYPEROPT BACKTESTING REPORT ================================') print('\n\n\n\n====================== HYPEROPT BACKTESTING REPORT ================================')
print('Best parameters {}'.format(best)) print('Best parameters {}'.format(best))
newlist = sorted(trials.results, key=itemgetter('loss')) newlist = sorted(trials.results, key=itemgetter('loss'))

View File

@ -1,5 +1,6 @@
# pragma pylint: disable=missing-docstring # pragma pylint: disable=missing-docstring
import copy import copy
from datetime import datetime
from unittest.mock import MagicMock, call from unittest.mock import MagicMock, call
import pytest import pytest
@ -48,6 +49,7 @@ def conf():
validate(configuration, CONF_SCHEMA) validate(configuration, CONF_SCHEMA)
return configuration return configuration
def test_create_trade(conf, mocker): def test_create_trade(conf, mocker):
mocker.patch.dict('freqtrade.main._CONF', conf) mocker.patch.dict('freqtrade.main._CONF', conf)
buy_signal = mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True) buy_signal = mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
@ -59,29 +61,43 @@ def test_create_trade(conf, mocker):
'ask': 0.072661, 'ask': 0.072661,
'last': 0.07256061 'last': 0.07256061
}), }),
buy=MagicMock(return_value='mocked_order_id')) buy=MagicMock(return_value='mocked_limit_buy'))
# Save state of current whitelist # Save state of current whitelist
whitelist = copy.deepcopy(conf['exchange']['pair_whitelist']) whitelist = copy.deepcopy(conf['exchange']['pair_whitelist'])
init(conf, 'sqlite://') init(conf, 'sqlite://')
for pair in ['BTC_ETH', 'BTC_TKN', 'BTC_TRST', 'BTC_SWT']: for _ in ['BTC_ETH', 'BTC_TKN', 'BTC_TRST', 'BTC_SWT']:
trade = create_trade(15.0) trade = create_trade(15.0)
Trade.session.add(trade) Trade.session.add(trade)
Trade.session.flush() Trade.session.flush()
assert trade is not None assert trade is not None
assert trade.open_rate == 0.072661
assert trade.pair == pair
assert trade.exchange == Exchanges.BITTREX.name
assert trade.amount == 206.43811673387373
assert trade.stake_amount == 15.0 assert trade.stake_amount == 15.0
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
# Simulate fulfilled LIMIT_BUY order for trade
trade.update({
'id': 'mocked_limit_buy',
'type': 'LIMIT_BUY',
'pair': 'mocked',
'opened': datetime.utcnow(),
'rate': 0.072661,
'amount': 206.43811673387373,
'remaining': 0.0,
'closed': datetime.utcnow(),
})
assert trade.open_rate == 0.072661
assert trade.amount == 206.43811673387373
assert whitelist == conf['exchange']['pair_whitelist'] assert whitelist == conf['exchange']['pair_whitelist']
buy_signal.assert_has_calls( buy_signal.assert_has_calls(
[call('BTC_ETH'), call('BTC_TKN'), call('BTC_TRST'), call('BTC_SWT')] [call('BTC_ETH'), call('BTC_TKN'), call('BTC_TRST'), call('BTC_SWT')]
) )
def test_handle_trade(conf, mocker): def test_handle_trade(conf, mocker):
mocker.patch.dict('freqtrade.main._CONF', conf) mocker.patch.dict('freqtrade.main._CONF', conf)
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock()) mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
@ -92,14 +108,29 @@ def test_handle_trade(conf, mocker):
'ask': 0.172661, 'ask': 0.172661,
'last': 0.17256061 'last': 0.17256061
}), }),
buy=MagicMock(return_value='mocked_order_id')) sell=MagicMock(return_value='mocked_limit_sell'))
trade = Trade.query.filter(Trade.is_open.is_(True)).first() trade = Trade.query.filter(Trade.is_open.is_(True)).first()
assert trade assert trade
handle_trade(trade) handle_trade(trade)
assert trade.open_order_id == 'mocked_limit_sell'
# Simulate fulfilled LIMIT_SELL order for trade
trade.update({
'id': 'mocked_sell_limit',
'type': 'LIMIT_SELL',
'pair': 'mocked',
'opened': datetime.utcnow(),
'rate': 0.17256061,
'amount': 206.43811673387373,
'remaining': 0.0,
'closed': datetime.utcnow(),
})
assert trade.close_rate == 0.17256061 assert trade.close_rate == 0.17256061
assert trade.close_profit == 137.4872490056564 assert trade.close_profit == 1.3698725
assert trade.close_date is not None assert trade.close_date is not None
assert trade.open_order_id == 'dry_run'
def test_close_trade(conf, mocker): def test_close_trade(conf, mocker):
mocker.patch.dict('freqtrade.main._CONF', conf) mocker.patch.dict('freqtrade.main._CONF', conf)
@ -113,14 +144,17 @@ def test_close_trade(conf, mocker):
assert closed assert closed
assert not trade.is_open assert not trade.is_open
def test_balance_fully_ask_side(mocker): def test_balance_fully_ask_side(mocker):
mocker.patch.dict('freqtrade.main._CONF', {'bid_strategy': {'ask_last_balance': 0.0}}) mocker.patch.dict('freqtrade.main._CONF', {'bid_strategy': {'ask_last_balance': 0.0}})
assert get_target_bid({'ask': 20, 'last': 10}) == 20 assert get_target_bid({'ask': 20, 'last': 10}) == 20
def test_balance_fully_last_side(mocker): def test_balance_fully_last_side(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': 20, 'last': 10}) == 10 assert get_target_bid({'ask': 20, 'last': 10}) == 10
def test_balance_when_last_bigger_than_ask(mocker): def test_balance_when_last_bigger_than_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

View File

@ -1,20 +0,0 @@
# pragma pylint: disable=missing-docstring
from freqtrade.exchange import Exchanges
from freqtrade.persistence import Trade
def test_exec_sell_order(mocker):
api_mock = mocker.patch('freqtrade.main.exchange.sell', side_effect='mocked_order_id')
trade = Trade(
pair='BTC_ETH',
stake_amount=1.00,
open_rate=0.50,
amount=10.00,
exchange=Exchanges.BITTREX,
open_order_id='mocked'
)
profit = trade.exec_sell_order(1.00, 10.00)
api_mock.assert_called_once_with('BTC_ETH', 1.0, 10.0)
assert profit == 100.0
assert trade.close_rate == 1.0
assert trade.close_profit == profit
assert trade.close_date is not None

View File

@ -11,12 +11,9 @@ from telegram import Bot, Update, Message, Chat
from freqtrade.main import init, create_trade from freqtrade.main import init, create_trade
from freqtrade.misc import update_state, State, get_state, CONF_SCHEMA from freqtrade.misc import update_state, State, get_state, CONF_SCHEMA
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.rpc.telegram import _status, _status_table, _profit, _forcesell, _performance, \ from freqtrade.rpc.telegram import (
_count, _start, _stop _status, _status_table, _profit, _forcesell, _performance, _count, _start, _stop, _balance
)
logging.getLogger('requests.packages.urllib3').setLevel(logging.INFO)
logging.getLogger('telegram').setLevel(logging.INFO)
logger = logging.getLogger(__name__)
@pytest.fixture @pytest.fixture
@ -54,6 +51,7 @@ def conf():
validate(configuration, CONF_SCHEMA) validate(configuration, CONF_SCHEMA)
return configuration return configuration
@pytest.fixture @pytest.fixture
def update(): def update():
_update = Update(0) _update = Update(0)
@ -69,7 +67,10 @@ def test_status_handle(conf, update, mocker):
mocker.patch.dict('freqtrade.main._CONF', conf) mocker.patch.dict('freqtrade.main._CONF', conf)
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True) mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
msg_mock = MagicMock() msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock) mocker.patch.multiple('freqtrade.main.telegram',
_CONF=conf,
init=MagicMock(),
send_msg=msg_mock)
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={
@ -86,8 +87,26 @@ def test_status_handle(conf, update, mocker):
Trade.session.add(trade) Trade.session.add(trade)
Trade.session.flush() Trade.session.flush()
# Trigger status while we don't know the open_rate yet
_status(bot=MagicBot(), update=update) _status(bot=MagicBot(), update=update)
assert msg_mock.call_count == 2
# Simulate fulfilled LIMIT_BUY order for trade
trade.update({
'id': 'mocked_limit_buy',
'type': 'LIMIT_BUY',
'pair': 'mocked',
'opened': datetime.utcnow(),
'rate': 0.07256060,
'amount': 206.43811673387373,
'remaining': 0.0,
'closed': datetime.utcnow(),
})
Trade.session.flush()
# Trigger status while we have a fulfilled order for the open trade
_status(bot=MagicBot(), update=update)
assert msg_mock.call_count == 3
assert '[BTC_ETH]' in msg_mock.call_args_list[-1][0][0] assert '[BTC_ETH]' in msg_mock.call_args_list[-1][0][0]
@ -127,7 +146,10 @@ def test_profit_handle(conf, update, mocker):
mocker.patch.dict('freqtrade.main._CONF', conf) mocker.patch.dict('freqtrade.main._CONF', conf)
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True) mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
msg_mock = MagicMock() msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock) mocker.patch.multiple('freqtrade.main.telegram',
_CONF=conf,
init=MagicMock(),
send_msg=msg_mock)
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={
@ -135,14 +157,36 @@ def test_profit_handle(conf, update, mocker):
'ask': 0.072661, 'ask': 0.072661,
'last': 0.07256061 'last': 0.07256061
}), }),
buy=MagicMock(return_value='mocked_order_id')) buy=MagicMock(return_value='mocked_limit_buy'))
init(conf, 'sqlite://') init(conf, 'sqlite://')
# Create some test data # Create some test data
trade = create_trade(15.0) trade = create_trade(15.0)
assert trade assert trade
trade.close_rate = 0.07256061
trade.close_profit = 100.00 # Simulate fulfilled LIMIT_BUY order for trade
trade.update({
'id': 'mocked_limit_buy',
'type': 'LIMIT_BUY',
'pair': 'mocked',
'opened': datetime.utcnow(),
'rate': 0.07256061,
'amount': 206.43811673387373,
'remaining': 0.0,
'closed': datetime.utcnow(),
})
# Simulate fulfilled LIMIT_SELL order for trade
trade.update({
'id': 'mocked_limit_sell',
'type': 'LIMIT_SELL',
'pair': 'mocked',
'opened': datetime.utcnow(),
'rate': 0.0802134,
'amount': 206.43811673387373,
'remaining': 0.0,
'closed': datetime.utcnow(),
})
trade.close_date = datetime.utcnow() trade.close_date = datetime.utcnow()
trade.open_order_id = None trade.open_order_id = None
trade.is_open = False trade.is_open = False
@ -151,13 +195,18 @@ def test_profit_handle(conf, update, mocker):
_profit(bot=MagicBot(), update=update) _profit(bot=MagicBot(), update=update)
assert msg_mock.call_count == 2 assert msg_mock.call_count == 2
assert '(100.00%)' in msg_mock.call_args_list[-1][0][0] assert '*ROI:* `1.507013 (10.05%)`' 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]
def test_forcesell_handle(conf, update, mocker): def test_forcesell_handle(conf, update, mocker):
mocker.patch.dict('freqtrade.main._CONF', conf) mocker.patch.dict('freqtrade.main._CONF', conf)
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True) mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
msg_mock = MagicMock() msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock) mocker.patch.multiple('freqtrade.main.telegram',
_CONF=conf,
init=MagicMock(),
send_msg=msg_mock)
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={
@ -171,6 +220,19 @@ def test_forcesell_handle(conf, update, mocker):
# Create some test data # Create some test data
trade = create_trade(15.0) trade = create_trade(15.0)
assert trade assert trade
# Simulate fulfilled LIMIT_BUY order for trade
trade.update({
'id': 'mocked_limit_buy',
'type': 'LIMIT_BUY',
'pair': 'mocked',
'opened': datetime.utcnow(),
'rate': 0.07256060,
'amount': 206.43811673387373,
'remaining': 0.0,
'closed': datetime.utcnow(),
})
Trade.session.add(trade) Trade.session.add(trade)
Trade.session.flush() Trade.session.flush()
@ -179,13 +241,17 @@ def test_forcesell_handle(conf, update, mocker):
assert msg_mock.call_count == 2 assert msg_mock.call_count == 2
assert 'Selling [BTC/ETH]' in msg_mock.call_args_list[-1][0][0] assert 'Selling [BTC/ETH]' in msg_mock.call_args_list[-1][0][0]
assert '0.072561' in msg_mock.call_args_list[-1][0][0] assert '0.072561 (profit: ~-0.5%)' in msg_mock.call_args_list[-1][0][0]
def test_performance_handle(conf, update, mocker): def test_performance_handle(conf, update, mocker):
mocker.patch.dict('freqtrade.main._CONF', conf) mocker.patch.dict('freqtrade.main._CONF', conf)
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True) mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
msg_mock = MagicMock() msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock) mocker.patch.multiple('freqtrade.main.telegram',
_CONF=conf,
init=MagicMock(),
send_msg=msg_mock)
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={
@ -199,10 +265,32 @@ def test_performance_handle(conf, update, mocker):
# Create some test data # Create some test data
trade = create_trade(15.0) trade = create_trade(15.0)
assert trade assert trade
trade.close_rate = 0.07256061
trade.close_profit = 100.00 # Simulate fulfilled LIMIT_BUY order for trade
trade.update({
'id': 'mocked_limit_buy',
'type': 'LIMIT_BUY',
'pair': 'mocked',
'opened': datetime.utcnow(),
'rate': 0.07256061,
'amount': 206.43811673387373,
'remaining': 0.0,
'closed': datetime.utcnow(),
})
# Simulate fulfilled LIMIT_SELL order for trade
trade.update({
'id': 'mocked_limit_sell',
'type': 'LIMIT_SELL',
'pair': 'mocked',
'opened': datetime.utcnow(),
'rate': 0.0802134,
'amount': 206.43811673387373,
'remaining': 0.0,
'closed': datetime.utcnow(),
})
trade.close_date = datetime.utcnow() trade.close_date = datetime.utcnow()
trade.open_order_id = None
trade.is_open = False trade.is_open = False
Trade.session.add(trade) Trade.session.add(trade)
Trade.session.flush() Trade.session.flush()
@ -210,7 +298,8 @@ def test_performance_handle(conf, update, mocker):
_performance(bot=MagicBot(), update=update) _performance(bot=MagicBot(), update=update)
assert msg_mock.call_count == 2 assert msg_mock.call_count == 2
assert 'Performance' in msg_mock.call_args_list[-1][0][0] assert 'Performance' in msg_mock.call_args_list[-1][0][0]
assert 'BTC_ETH 100.00%' in msg_mock.call_args_list[-1][0][0] assert '<code>BTC_ETH\t10.05%</code>' in msg_mock.call_args_list[-1][0][0]
def test_count_handle(conf, update, mocker): def test_count_handle(conf, update, mocker):
@ -245,8 +334,13 @@ def test_count_handle(conf, update, mocker):
def test_start_handle(conf, update, mocker): def test_start_handle(conf, update, mocker):
mocker.patch.dict('freqtrade.main._CONF', conf) mocker.patch.dict('freqtrade.main._CONF', conf)
msg_mock = MagicMock() msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock) mocker.patch.multiple('freqtrade.main.telegram',
mocker.patch.multiple('freqtrade.main.exchange', _CONF=conf, init=MagicMock()) _CONF=conf,
init=MagicMock(),
send_msg=msg_mock)
mocker.patch.multiple('freqtrade.main.exchange',
_CONF=conf,
init=MagicMock())
init(conf, 'sqlite://') init(conf, 'sqlite://')
update_state(State.STOPPED) update_state(State.STOPPED)
@ -255,11 +349,17 @@ def test_start_handle(conf, update, mocker):
assert get_state() == State.RUNNING assert get_state() == State.RUNNING
assert msg_mock.call_count == 0 assert msg_mock.call_count == 0
def test_stop_handle(conf, update, mocker): def test_stop_handle(conf, update, mocker):
mocker.patch.dict('freqtrade.main._CONF', conf) mocker.patch.dict('freqtrade.main._CONF', conf)
msg_mock = MagicMock() msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock) mocker.patch.multiple('freqtrade.main.telegram',
mocker.patch.multiple('freqtrade.main.exchange', _CONF=conf, init=MagicMock()) _CONF=conf,
init=MagicMock(),
send_msg=msg_mock)
mocker.patch.multiple('freqtrade.main.exchange',
_CONF=conf,
init=MagicMock())
init(conf, 'sqlite://') init(conf, 'sqlite://')
update_state(State.RUNNING) update_state(State.RUNNING)
@ -268,3 +368,25 @@ def test_stop_handle(conf, update, mocker):
assert get_state() == State.STOPPED assert get_state() == State.STOPPED
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert 'Stopping trader' in msg_mock.call_args_list[0][0][0] assert 'Stopping trader' in msg_mock.call_args_list[0][0][0]
def test_balance_handle(conf, update, mocker):
mock_balance = [{
'Currency': 'BTC',
'Balance': 10.0,
'Available': 12.0,
'Pending': 0.0,
'CryptoAddress': 'XXXX'}]
mocker.patch.dict('freqtrade.main._CONF', conf)
msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.telegram',
_CONF=conf,
init=MagicMock(),
send_msg=msg_mock)
mocker.patch.multiple('freqtrade.main.exchange',
get_balances=MagicMock(return_value=mock_balance))
_balance(bot=MagicBot(), update=update)
assert msg_mock.call_count == 1
assert '*Currency*: BTC' in msg_mock.call_args_list[0][0][0]
assert 'Balance' in msg_mock.call_args_list[0][0][0]

0
freqtrade/vendor/__init__.py vendored Normal file
View File

0
freqtrade/vendor/qtpylib/__init__.py vendored Normal file
View File