Merge remote-tracking branch 'upstream/develop' into develop
This commit is contained in:
commit
f1730eee3a
@ -6,10 +6,10 @@
|
||||
"minimal_roi": {
|
||||
"40": 0.0,
|
||||
"30": 0.01,
|
||||
"20": 0.02
|
||||
"20": 0.02,
|
||||
"0": 0.04
|
||||
},
|
||||
"stoploss": -0.40,
|
||||
"stoploss": -0.10,
|
||||
"bid_strategy": {
|
||||
"ask_last_balance": 0.0
|
||||
},
|
||||
|
@ -1,4 +1,16 @@
|
||||
""" FreqTrade bot """
|
||||
__version__ = '0.14.3'
|
||||
|
||||
from . import main
|
||||
|
||||
class DependencyException(BaseException):
|
||||
"""
|
||||
Indicates that a assumed dependency is not met.
|
||||
This could happen when there is currently not enough money on the account.
|
||||
"""
|
||||
|
||||
|
||||
class OperationalException(BaseException):
|
||||
"""
|
||||
Requires manual intervention.
|
||||
This happens when an exchange returns an unexpected error during runtime.
|
||||
"""
|
||||
|
@ -1,9 +1,9 @@
|
||||
"""
|
||||
Functions to analyze ticker data with indicators and produce buy and sell signals
|
||||
"""
|
||||
from enum import Enum
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from enum import Enum
|
||||
|
||||
import arrow
|
||||
import talib.abstract as ta
|
||||
@ -61,6 +61,10 @@ def populate_indicators(dataframe: DataFrame) -> DataFrame:
|
||||
hilbert = ta.HT_SINE(dataframe)
|
||||
dataframe['htsine'] = hilbert['sine']
|
||||
dataframe['htleadsine'] = hilbert['leadsine']
|
||||
dataframe['plus_dm'] = ta.PLUS_DM(dataframe)
|
||||
dataframe['plus_di'] = ta.PLUS_DI(dataframe)
|
||||
dataframe['minus_dm'] = ta.MINUS_DM(dataframe)
|
||||
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
|
||||
return dataframe
|
||||
|
||||
|
||||
@ -71,14 +75,21 @@ def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
|
||||
:return: DataFrame with buy column
|
||||
"""
|
||||
dataframe.loc[
|
||||
(dataframe['tema'] <= dataframe['blower']) &
|
||||
(dataframe['rsi'] < 37) &
|
||||
(dataframe['fastd'] < 48) &
|
||||
(dataframe['adx'] > 31),
|
||||
(
|
||||
(dataframe['rsi'] < 35) &
|
||||
(dataframe['fastd'] < 35) &
|
||||
(dataframe['adx'] > 30) &
|
||||
(dataframe['plus_di'] > 0.5)
|
||||
) |
|
||||
(
|
||||
(dataframe['adx'] > 65) &
|
||||
(dataframe['plus_di'] > 0.5)
|
||||
),
|
||||
'buy'] = 1
|
||||
|
||||
return dataframe
|
||||
|
||||
|
||||
def populate_sell_trend(dataframe: DataFrame) -> DataFrame:
|
||||
"""
|
||||
Based on TA indicators, populates the sell signal for the given dataframe
|
||||
@ -86,9 +97,19 @@ def populate_sell_trend(dataframe: DataFrame) -> DataFrame:
|
||||
:return: DataFrame with buy column
|
||||
"""
|
||||
dataframe.loc[
|
||||
(crossed_above(dataframe['rsi'], 70)),
|
||||
(
|
||||
(
|
||||
(crossed_above(dataframe['rsi'], 70)) |
|
||||
(crossed_above(dataframe['fastd'], 70))
|
||||
) &
|
||||
(dataframe['adx'] > 10) &
|
||||
(dataframe['minus_di'] > 0)
|
||||
) |
|
||||
(
|
||||
(dataframe['adx'] > 70) &
|
||||
(dataframe['minus_di'] > 0.5)
|
||||
),
|
||||
'sell'] = 1
|
||||
|
||||
return dataframe
|
||||
|
||||
|
||||
@ -107,9 +128,6 @@ def analyze_ticker(pair: str) -> DataFrame:
|
||||
dataframe = populate_indicators(dataframe)
|
||||
dataframe = populate_buy_trend(dataframe)
|
||||
dataframe = populate_sell_trend(dataframe)
|
||||
# TODO: buy_price and sell_price are only used by the plotter, should probably be moved there
|
||||
dataframe.loc[dataframe['buy'] == 1, 'buy_price'] = dataframe['close']
|
||||
dataframe.loc[dataframe['sell'] == 1, 'sell_price'] = dataframe['close']
|
||||
return dataframe
|
||||
|
||||
|
||||
@ -119,7 +137,12 @@ def get_signal(pair: str, signal: SignalType) -> bool:
|
||||
:param pair: pair in format BTC_ANT or BTC-ANT
|
||||
:return: True if pair is good for buying, False otherwise
|
||||
"""
|
||||
try:
|
||||
dataframe = analyze_ticker(pair)
|
||||
except ValueError as ex:
|
||||
logger.warning('Unable to analyze ticker for pair %s: %s', pair, str(ex))
|
||||
return False
|
||||
|
||||
if dataframe.empty:
|
||||
return False
|
||||
|
||||
|
@ -9,6 +9,7 @@ import arrow
|
||||
import requests
|
||||
from cachetools import cached, TTLCache
|
||||
|
||||
from freqtrade import OperationalException
|
||||
from freqtrade.exchange.bittrex import Bittrex
|
||||
from freqtrade.exchange.interface import Exchange
|
||||
|
||||
@ -51,7 +52,7 @@ def init(config: dict) -> None:
|
||||
try:
|
||||
exchange_class = Exchanges[name.upper()].value
|
||||
except KeyError:
|
||||
raise RuntimeError('Exchange {} is not supported'.format(name))
|
||||
raise OperationalException('Exchange {} is not supported'.format(name))
|
||||
|
||||
_API = exchange_class(exchange_config)
|
||||
|
||||
@ -62,7 +63,7 @@ def init(config: dict) -> None:
|
||||
def validate_pairs(pairs: List[str]) -> None:
|
||||
"""
|
||||
Checks if all given pairs are tradable on the current exchange.
|
||||
Raises RuntimeError if one pair is not available.
|
||||
Raises OperationalException if one pair is not available.
|
||||
:param pairs: list of pairs
|
||||
:return: None
|
||||
"""
|
||||
@ -75,11 +76,12 @@ def validate_pairs(pairs: List[str]) -> None:
|
||||
stake_cur = _CONF['stake_currency']
|
||||
for pair in pairs:
|
||||
if not pair.startswith(stake_cur):
|
||||
raise RuntimeError(
|
||||
raise OperationalException(
|
||||
'Pair {} not compatible with stake_currency: {}'.format(pair, stake_cur)
|
||||
)
|
||||
if pair not in markets:
|
||||
raise RuntimeError('Pair {} is not available at {}'.format(pair, _API.name.lower()))
|
||||
raise OperationalException(
|
||||
'Pair {} is not available at {}'.format(pair, _API.name.lower()))
|
||||
|
||||
|
||||
def buy(pair: str, rate: float, amount: float) -> str:
|
||||
|
@ -4,6 +4,7 @@ from typing import List, Dict
|
||||
from bittrex.bittrex import Bittrex as _Bittrex, API_V2_0, API_V1_1
|
||||
from requests.exceptions import ContentDecodingError
|
||||
|
||||
from freqtrade import OperationalException
|
||||
from freqtrade.exchange.interface import Exchange
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -46,7 +47,7 @@ class Bittrex(Exchange):
|
||||
def buy(self, pair: str, rate: float, amount: float) -> str:
|
||||
data = _API.buy_limit(pair.replace('_', '-'), amount, rate)
|
||||
if not data['success']:
|
||||
raise RuntimeError('{message} params=({pair}, {rate}, {amount})'.format(
|
||||
raise OperationalException('{message} params=({pair}, {rate}, {amount})'.format(
|
||||
message=data['message'],
|
||||
pair=pair,
|
||||
rate=rate,
|
||||
@ -56,7 +57,7 @@ class Bittrex(Exchange):
|
||||
def sell(self, pair: str, rate: float, amount: float) -> str:
|
||||
data = _API.sell_limit(pair.replace('_', '-'), amount, rate)
|
||||
if not data['success']:
|
||||
raise RuntimeError('{message} params=({pair}, {rate}, {amount})'.format(
|
||||
raise OperationalException('{message} params=({pair}, {rate}, {amount})'.format(
|
||||
message=data['message'],
|
||||
pair=pair,
|
||||
rate=rate,
|
||||
@ -66,7 +67,7 @@ class Bittrex(Exchange):
|
||||
def get_balance(self, currency: str) -> float:
|
||||
data = _API.get_balance(currency)
|
||||
if not data['success']:
|
||||
raise RuntimeError('{message} params=({currency})'.format(
|
||||
raise OperationalException('{message} params=({currency})'.format(
|
||||
message=data['message'],
|
||||
currency=currency))
|
||||
return float(data['result']['Balance'] or 0.0)
|
||||
@ -74,13 +75,13 @@ class Bittrex(Exchange):
|
||||
def get_balances(self):
|
||||
data = _API.get_balances()
|
||||
if not data['success']:
|
||||
raise RuntimeError('{message}'.format(message=data['message']))
|
||||
raise OperationalException('{message}'.format(message=data['message']))
|
||||
return data['result']
|
||||
|
||||
def get_ticker(self, pair: str) -> dict:
|
||||
data = _API.get_ticker(pair.replace('_', '-'))
|
||||
if not data['success']:
|
||||
raise RuntimeError('{message} params=({pair})'.format(
|
||||
raise OperationalException('{message} params=({pair})'.format(
|
||||
message=data['message'],
|
||||
pair=pair))
|
||||
|
||||
@ -121,7 +122,7 @@ class Bittrex(Exchange):
|
||||
pair=pair))
|
||||
|
||||
if not data['success']:
|
||||
raise RuntimeError('{message} params=({pair})'.format(
|
||||
raise OperationalException('{message} params=({pair})'.format(
|
||||
message=data['message'],
|
||||
pair=pair))
|
||||
|
||||
@ -130,7 +131,7 @@ class Bittrex(Exchange):
|
||||
def get_order(self, order_id: str) -> Dict:
|
||||
data = _API.get_order(order_id)
|
||||
if not data['success']:
|
||||
raise RuntimeError('{message} params=({order_id})'.format(
|
||||
raise OperationalException('{message} params=({order_id})'.format(
|
||||
message=data['message'],
|
||||
order_id=order_id))
|
||||
data = data['result']
|
||||
@ -148,7 +149,7 @@ class Bittrex(Exchange):
|
||||
def cancel_order(self, order_id: str) -> None:
|
||||
data = _API.cancel(order_id)
|
||||
if not data['success']:
|
||||
raise RuntimeError('{message} params=({order_id})'.format(
|
||||
raise OperationalException('{message} params=({order_id})'.format(
|
||||
message=data['message'],
|
||||
order_id=order_id))
|
||||
|
||||
@ -158,19 +159,19 @@ class Bittrex(Exchange):
|
||||
def get_markets(self) -> List[str]:
|
||||
data = _API.get_markets()
|
||||
if not data['success']:
|
||||
raise RuntimeError('{message}'.format(message=data['message']))
|
||||
raise OperationalException('{message}'.format(message=data['message']))
|
||||
return [m['MarketName'].replace('-', '_') for m in data['result']]
|
||||
|
||||
def get_market_summaries(self) -> List[Dict]:
|
||||
data = _API.get_market_summaries()
|
||||
if not data['success']:
|
||||
raise RuntimeError('{message}'.format(message=data['message']))
|
||||
raise OperationalException('{message}'.format(message=data['message']))
|
||||
return data['result']
|
||||
|
||||
def get_wallet_health(self) -> List[Dict]:
|
||||
data = _API_V2.get_wallet_health()
|
||||
if not data['success']:
|
||||
raise RuntimeError('{message}'.format(message=data['message']))
|
||||
raise OperationalException('{message}'.format(message=data['message']))
|
||||
return [{
|
||||
'Currency': entry['Health']['Currency'],
|
||||
'IsActive': entry['Health']['IsActive'],
|
||||
|
@ -6,16 +6,16 @@ import sys
|
||||
import time
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
from signal import signal, SIGINT, SIGABRT, SIGTERM
|
||||
from typing import Dict, Optional, List
|
||||
|
||||
import requests
|
||||
from cachetools import cached, TTLCache
|
||||
|
||||
from freqtrade import __version__, exchange, persistence, rpc
|
||||
from freqtrade import __version__, exchange, persistence, rpc, DependencyException, \
|
||||
OperationalException
|
||||
from freqtrade.analyze import get_signal, SignalType
|
||||
from freqtrade.misc import State, get_state, update_state, parse_args, throttle, \
|
||||
load_config, FreqtradeException
|
||||
load_config
|
||||
from freqtrade.persistence import Trade
|
||||
|
||||
logger = logging.getLogger('freqtrade')
|
||||
@ -67,16 +67,13 @@ def _process(dynamic_whitelist: Optional[bool] = False) -> bool:
|
||||
if len(trades) < _CONF['max_open_trades']:
|
||||
try:
|
||||
# Create entity and execute trade
|
||||
trade = create_trade(float(_CONF['stake_amount']))
|
||||
if trade:
|
||||
Trade.session.add(trade)
|
||||
state_changed = True
|
||||
else:
|
||||
state_changed = create_trade(float(_CONF['stake_amount']))
|
||||
if not state_changed:
|
||||
logger.info(
|
||||
'Checked all whitelisted currencies. '
|
||||
'Found no suitable entry positions for buying. Will keep looking ...'
|
||||
)
|
||||
except FreqtradeException as e:
|
||||
except DependencyException as e:
|
||||
logger.warning('Unable to create trade: %s', e)
|
||||
|
||||
for trade in trades:
|
||||
@ -97,12 +94,12 @@ def _process(dynamic_whitelist: Optional[bool] = False) -> bool:
|
||||
error
|
||||
)
|
||||
time.sleep(30)
|
||||
except RuntimeError:
|
||||
rpc.send_msg('*Status:* Got RuntimeError:\n```\n{traceback}```{hint}'.format(
|
||||
except OperationalException:
|
||||
rpc.send_msg('*Status:* Got OperationalException:\n```\n{traceback}```{hint}'.format(
|
||||
traceback=traceback.format_exc(),
|
||||
hint='Issue `/start` if you think it is safe to restart.'
|
||||
))
|
||||
logger.exception('Got RuntimeError. Stopping trader ...')
|
||||
logger.exception('Got OperationalException. Stopping trader ...')
|
||||
update_state(State.STOPPED)
|
||||
return state_changed
|
||||
|
||||
@ -126,6 +123,7 @@ def execute_sell(trade: Trade, limit: float) -> None:
|
||||
limit,
|
||||
fmt_exp_profit
|
||||
))
|
||||
Trade.session.flush()
|
||||
|
||||
|
||||
def min_roi_reached(trade: Trade, current_rate: float, current_time: datetime) -> bool:
|
||||
@ -172,11 +170,12 @@ def get_target_bid(ticker: Dict[str, float]) -> float:
|
||||
return ticker['ask'] + balance * (ticker['last'] - ticker['ask'])
|
||||
|
||||
|
||||
def create_trade(stake_amount: float) -> Optional[Trade]:
|
||||
def create_trade(stake_amount: float) -> bool:
|
||||
"""
|
||||
Checks the implemented trading indicator(s) for a randomly picked pair,
|
||||
if one pair triggers the buy_signal a new trade record gets created
|
||||
:param stake_amount: amount of btc to spend
|
||||
:return: True if a trade object has been created and persisted, False otherwise
|
||||
"""
|
||||
logger.info(
|
||||
'Checking buy signals to create a new trade with stake_amount: %f ...',
|
||||
@ -185,7 +184,7 @@ def create_trade(stake_amount: float) -> Optional[Trade]:
|
||||
whitelist = copy.deepcopy(_CONF['exchange']['pair_whitelist'])
|
||||
# Check if stake_amount is fulfilled
|
||||
if exchange.get_balance(_CONF['stake_currency']) < stake_amount:
|
||||
raise FreqtradeException(
|
||||
raise DependencyException(
|
||||
'stake amount is not fulfilled (currency={})'.format(_CONF['stake_currency'])
|
||||
)
|
||||
|
||||
@ -195,7 +194,7 @@ def create_trade(stake_amount: float) -> Optional[Trade]:
|
||||
whitelist.remove(trade.pair)
|
||||
logger.debug('Ignoring %s in pair whitelist', trade.pair)
|
||||
if not whitelist:
|
||||
raise FreqtradeException('No pair in whitelist')
|
||||
raise DependencyException('No pair in whitelist')
|
||||
|
||||
# Pick pair based on StochRSI buy signals
|
||||
for _pair in whitelist:
|
||||
@ -203,12 +202,11 @@ def create_trade(stake_amount: float) -> Optional[Trade]:
|
||||
pair = _pair
|
||||
break
|
||||
else:
|
||||
return None
|
||||
return False
|
||||
|
||||
# Calculate amount and subtract fee
|
||||
fee = exchange.get_fee()
|
||||
# Calculate amount
|
||||
buy_limit = get_target_bid(exchange.get_ticker(pair))
|
||||
amount = (1 - fee) * stake_amount / buy_limit
|
||||
amount = stake_amount / buy_limit
|
||||
|
||||
order_id = exchange.buy(pair, buy_limit, amount)
|
||||
# Create trade entity and return
|
||||
@ -219,14 +217,19 @@ def create_trade(stake_amount: float) -> Optional[Trade]:
|
||||
buy_limit
|
||||
))
|
||||
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
|
||||
return Trade(pair=pair,
|
||||
trade = Trade(
|
||||
pair=pair,
|
||||
stake_amount=stake_amount,
|
||||
amount=amount,
|
||||
fee=fee * 2,
|
||||
fee=exchange.get_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
|
||||
)
|
||||
Trade.session.add(trade)
|
||||
Trade.session.flush()
|
||||
return True
|
||||
|
||||
|
||||
def init(config: dict, db_url: Optional[str] = None) -> None:
|
||||
@ -248,10 +251,6 @@ def init(config: dict, db_url: Optional[str] = None) -> None:
|
||||
else:
|
||||
update_state(State.STOPPED)
|
||||
|
||||
# Register signal handlers
|
||||
for sig in (SIGINT, SIGTERM, SIGABRT):
|
||||
signal(sig, cleanup)
|
||||
|
||||
|
||||
@cached(TTLCache(maxsize=1, ttl=1800))
|
||||
def gen_pair_whitelist(base_currency: str, topn: int = 20, key: str = 'BaseVolume') -> List[str]:
|
||||
@ -270,7 +269,7 @@ def gen_pair_whitelist(base_currency: str, topn: int = 20, key: str = 'BaseVolum
|
||||
return [s['MarketName'].replace('-', '_') for s in summaries[:topn]]
|
||||
|
||||
|
||||
def cleanup(*args, **kwargs) -> None:
|
||||
def cleanup() -> None:
|
||||
"""
|
||||
Cleanup the application state und finish all pending tasks
|
||||
:return: None
|
||||
@ -283,7 +282,7 @@ def cleanup(*args, **kwargs) -> None:
|
||||
exit(0)
|
||||
|
||||
|
||||
def main():
|
||||
def main() -> None:
|
||||
"""
|
||||
Loads and validates the config and handles the main loop
|
||||
:return: None
|
||||
@ -311,6 +310,8 @@ def main():
|
||||
# Initialize all modules and start main loop
|
||||
if args.dynamic_whitelist:
|
||||
logger.info('Using dynamically generated whitelist. (--dynamic-whitelist detected)')
|
||||
|
||||
try:
|
||||
init(_CONF)
|
||||
old_state = None
|
||||
while True:
|
||||
@ -329,6 +330,12 @@ def main():
|
||||
dynamic_whitelist=args.dynamic_whitelist,
|
||||
)
|
||||
old_state = new_state
|
||||
except KeyboardInterrupt:
|
||||
logger.info('Got SIGINT, aborting ...')
|
||||
except BaseException:
|
||||
logger.exception('Got fatal exception!')
|
||||
finally:
|
||||
cleanup()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
@ -15,10 +15,6 @@ from freqtrade import __version__
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FreqtradeException(BaseException):
|
||||
pass
|
||||
|
||||
|
||||
class State(enum.Enum):
|
||||
RUNNING = 0
|
||||
STOPPED = 1
|
||||
@ -150,6 +146,12 @@ def build_subcommands(parser: argparse.ArgumentParser) -> None:
|
||||
type=int,
|
||||
metavar='INT',
|
||||
)
|
||||
backtest.add_argument(
|
||||
'--limit-max-trades',
|
||||
help='uses max_open_trades from config to simulate real world limitations',
|
||||
action='store_true',
|
||||
dest='limit_max_trades',
|
||||
)
|
||||
|
||||
|
||||
def start_backtesting(args) -> None:
|
||||
@ -165,6 +167,7 @@ def start_backtesting(args) -> None:
|
||||
'BACKTEST_LIVE': 'true' if args.live else '',
|
||||
'BACKTEST_CONFIG': args.config,
|
||||
'BACKTEST_TICKER_INTERVAL': str(args.ticker_interval),
|
||||
'BACKTEST_LIMIT_MAX_TRADES': 'true' if args.limit_max_trades else '',
|
||||
})
|
||||
path = os.path.join(os.path.dirname(__file__), 'tests', 'test_backtesting.py')
|
||||
pytest.main(['-s', path])
|
||||
|
@ -2,11 +2,11 @@ import logging
|
||||
import re
|
||||
from datetime import timedelta
|
||||
from typing import Callable, Any
|
||||
from pandas import DataFrame
|
||||
from tabulate import tabulate
|
||||
|
||||
import arrow
|
||||
from pandas import DataFrame
|
||||
from sqlalchemy import and_, func, text
|
||||
from tabulate import tabulate
|
||||
from telegram import ParseMode, Bot, Update
|
||||
from telegram.error import NetworkError, TelegramError
|
||||
from telegram.ext import CommandHandler, Updater
|
||||
|
@ -23,7 +23,7 @@ def default_conf():
|
||||
"20": 0.02,
|
||||
"0": 0.04
|
||||
},
|
||||
"stoploss": -0.05,
|
||||
"stoploss": -0.10,
|
||||
"bid_strategy": {
|
||||
"ask_last_balance": 0.0
|
||||
},
|
||||
@ -54,6 +54,7 @@ def default_conf():
|
||||
@pytest.fixture(scope="module")
|
||||
def backtest_conf():
|
||||
return {
|
||||
"max_open_trades": 3,
|
||||
"stake_currency": "BTC",
|
||||
"stake_amount": 0.01,
|
||||
"minimal_roi": {
|
||||
@ -62,7 +63,7 @@ def backtest_conf():
|
||||
"20": 0.02,
|
||||
"0": 0.04
|
||||
},
|
||||
"stoploss": -0.05
|
||||
"stoploss": -0.10
|
||||
}
|
||||
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
# pragma pylint: disable=missing-docstring,W0621
|
||||
import json
|
||||
|
||||
import arrow
|
||||
import pytest
|
||||
from pandas import DataFrame
|
||||
|
@ -83,24 +83,46 @@ def generate_text_table(data: Dict[str, Dict], results: DataFrame, stake_currenc
|
||||
return tabulate(tabular_data, headers=headers)
|
||||
|
||||
|
||||
def backtest(backtest_conf, processed, mocker):
|
||||
def backtest(config: Dict, processed, mocker, max_open_trades=0):
|
||||
"""
|
||||
Implements backtesting functionality
|
||||
:param config: config to use
|
||||
:param processed: a processed dictionary with format {pair, data}
|
||||
:param mocker: mocker instance
|
||||
:param max_open_trades: maximum number of concurrent trades (default: 0, disabled)
|
||||
:return: DataFrame
|
||||
"""
|
||||
trades = []
|
||||
trade_count_lock = {}
|
||||
exchange._API = Bittrex({'key': '', 'secret': ''})
|
||||
mocker.patch.dict('freqtrade.main._CONF', backtest_conf)
|
||||
mocker.patch.dict('freqtrade.main._CONF', config)
|
||||
for pair, pair_data in processed.items():
|
||||
pair_data['buy'] = 0
|
||||
pair_data['sell'] = 0
|
||||
pair_data['buy'], pair_data['sell'] = 0, 0
|
||||
ticker = populate_sell_trend(populate_buy_trend(pair_data))
|
||||
# for each buy point
|
||||
for row in ticker[ticker.buy == 1].itertuples(index=True):
|
||||
if max_open_trades > 0:
|
||||
# Check if max_open_trades has already been reached for the given date
|
||||
if not trade_count_lock.get(row.date, 0) < max_open_trades:
|
||||
continue
|
||||
|
||||
if max_open_trades > 0:
|
||||
# Increase lock
|
||||
trade_count_lock[row.date] = trade_count_lock.get(row.date, 0) + 1
|
||||
|
||||
trade = Trade(
|
||||
open_rate=row.close,
|
||||
open_date=row.date,
|
||||
amount=backtest_conf['stake_amount'],
|
||||
amount=config['stake_amount'],
|
||||
fee=exchange.get_fee() * 2
|
||||
)
|
||||
|
||||
# calculate win/lose forwards from buy point
|
||||
for row2 in ticker[row.Index:].itertuples(index=True):
|
||||
for row2 in ticker[row.Index + 1:].itertuples(index=True):
|
||||
if max_open_trades > 0:
|
||||
# Increase trade_count_lock for every iteration
|
||||
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:
|
||||
current_profit = trade.calc_profit(row2.close)
|
||||
|
||||
@ -110,6 +132,13 @@ def backtest(backtest_conf, processed, mocker):
|
||||
return DataFrame.from_records(trades, columns=labels)
|
||||
|
||||
|
||||
def get_max_open_trades(config):
|
||||
if not os.environ.get('BACKTEST_LIMIT_MAX_TRADES'):
|
||||
return 0
|
||||
print('Using max_open_trades: {} ...'.format(config['max_open_trades']))
|
||||
return config['max_open_trades']
|
||||
|
||||
|
||||
@pytest.mark.skipif(not os.environ.get('BACKTEST'), reason="BACKTEST not set")
|
||||
def test_backtest(backtest_conf, mocker):
|
||||
print('')
|
||||
@ -147,8 +176,6 @@ def test_backtest(backtest_conf, mocker):
|
||||
))
|
||||
|
||||
# Execute backtest and print results
|
||||
results = backtest(config, preprocess(data), mocker)
|
||||
print('====================== BACKTESTING REPORT ======================================\n\n'
|
||||
'NOTE: This Report doesn\'t respect the limits of max_open_trades, \n'
|
||||
' so the projected values should be taken with a grain of salt.\n')
|
||||
results = backtest(config, preprocess(data), mocker, get_max_open_trades(config))
|
||||
print('====================== BACKTESTING REPORT ======================================\n\n')
|
||||
print(generate_text_table(data, results, config['stake_currency']))
|
||||
|
@ -3,6 +3,7 @@ from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from freqtrade import OperationalException
|
||||
from freqtrade.exchange import validate_pairs
|
||||
|
||||
|
||||
@ -21,7 +22,7 @@ def test_validate_pairs_not_available(default_conf, mocker):
|
||||
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(RuntimeError, match=r'not available'):
|
||||
with pytest.raises(OperationalException, match=r'not available'):
|
||||
validate_pairs(default_conf['exchange']['pair_whitelist'])
|
||||
|
||||
|
||||
@ -31,5 +32,5 @@ def test_validate_pairs_not_compatible(default_conf, mocker):
|
||||
default_conf['stake_currency'] = 'ETH'
|
||||
mocker.patch('freqtrade.exchange._API', api_mock)
|
||||
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
|
||||
with pytest.raises(RuntimeError, match=r'not compatible'):
|
||||
with pytest.raises(OperationalException, match=r'not compatible'):
|
||||
validate_pairs(default_conf['exchange']['pair_whitelist'])
|
||||
|
@ -6,11 +6,12 @@ import pytest
|
||||
import requests
|
||||
from sqlalchemy import create_engine
|
||||
|
||||
from freqtrade.exchange import Exchanges
|
||||
from freqtrade import DependencyException, OperationalException
|
||||
from freqtrade.analyze import SignalType
|
||||
from freqtrade.exchange import Exchanges
|
||||
from freqtrade.main import create_trade, handle_trade, init, \
|
||||
get_target_bid, _process
|
||||
from freqtrade.misc import get_state, State, FreqtradeException
|
||||
from freqtrade.misc import get_state, State
|
||||
from freqtrade.persistence import Trade
|
||||
|
||||
|
||||
@ -40,7 +41,7 @@ def test_process_trade_creation(default_conf, ticker, health, mocker):
|
||||
assert trade.open_date is not None
|
||||
assert trade.exchange == Exchanges.BITTREX.name
|
||||
assert trade.open_rate == 0.072661
|
||||
assert trade.amount == 0.6864067381401302
|
||||
assert trade.amount == 0.6881270557795791
|
||||
|
||||
|
||||
def test_process_exchange_failures(default_conf, ticker, health, mocker):
|
||||
@ -59,7 +60,7 @@ def test_process_exchange_failures(default_conf, ticker, health, mocker):
|
||||
assert sleep_mock.has_calls()
|
||||
|
||||
|
||||
def test_process_runtime_error(default_conf, ticker, health, mocker):
|
||||
def test_process_operational_exception(default_conf, ticker, health, mocker):
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=msg_mock)
|
||||
@ -68,14 +69,14 @@ def test_process_runtime_error(default_conf, ticker, health, mocker):
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=ticker,
|
||||
get_wallet_health=health,
|
||||
buy=MagicMock(side_effect=RuntimeError))
|
||||
buy=MagicMock(side_effect=OperationalException))
|
||||
init(default_conf, create_engine('sqlite://'))
|
||||
assert get_state() == State.RUNNING
|
||||
|
||||
result = _process()
|
||||
assert result is False
|
||||
assert get_state() == State.STOPPED
|
||||
assert 'RuntimeError' in msg_mock.call_args_list[-1][0][0]
|
||||
assert 'OperationalException' in msg_mock.call_args_list[-1][0][0]
|
||||
|
||||
|
||||
def test_process_trade_handling(default_conf, ticker, limit_buy_order, health, mocker):
|
||||
@ -114,9 +115,9 @@ def test_create_trade(default_conf, ticker, limit_buy_order, mocker):
|
||||
whitelist = copy.deepcopy(default_conf['exchange']['pair_whitelist'])
|
||||
|
||||
init(default_conf, create_engine('sqlite://'))
|
||||
trade = create_trade(15.0)
|
||||
Trade.session.add(trade)
|
||||
Trade.session.flush()
|
||||
create_trade(15.0)
|
||||
|
||||
trade = Trade.query.first()
|
||||
assert trade is not None
|
||||
assert trade.stake_amount == 15.0
|
||||
assert trade.is_open
|
||||
@ -132,6 +133,21 @@ def test_create_trade(default_conf, ticker, limit_buy_order, mocker):
|
||||
assert whitelist == default_conf['exchange']['pair_whitelist']
|
||||
|
||||
|
||||
def test_create_trade_minimal_amount(default_conf, ticker, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||
buy_mock = mocker.patch('freqtrade.main.exchange.buy', MagicMock(return_value='mocked_limit_buy'))
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=ticker)
|
||||
init(default_conf, create_engine('sqlite://'))
|
||||
min_stake_amount = 0.0005
|
||||
create_trade(min_stake_amount)
|
||||
rate, amount = buy_mock.call_args[0][1], buy_mock.call_args[0][2]
|
||||
assert rate * amount >= min_stake_amount
|
||||
|
||||
|
||||
def test_create_trade_no_stake_amount(default_conf, ticker, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||
@ -141,7 +157,7 @@ def test_create_trade_no_stake_amount(default_conf, ticker, mocker):
|
||||
get_ticker=ticker,
|
||||
buy=MagicMock(return_value='mocked_limit_buy'),
|
||||
get_balance=MagicMock(return_value=default_conf['stake_amount'] * 0.5))
|
||||
with pytest.raises(FreqtradeException, match=r'.*stake amount.*'):
|
||||
with pytest.raises(DependencyException, match=r'.*stake amount.*'):
|
||||
create_trade(default_conf['stake_amount'])
|
||||
|
||||
|
||||
@ -154,7 +170,7 @@ def test_create_trade_no_pairs(default_conf, ticker, mocker):
|
||||
get_ticker=ticker,
|
||||
buy=MagicMock(return_value='mocked_limit_buy'))
|
||||
|
||||
with pytest.raises(FreqtradeException, match=r'.*No pair in whitelist.*'):
|
||||
with pytest.raises(DependencyException, match=r'.*No pair in whitelist.*'):
|
||||
conf = copy.deepcopy(default_conf)
|
||||
conf['exchange']['pair_whitelist'] = []
|
||||
mocker.patch.dict('freqtrade.main._CONF', conf)
|
||||
@ -175,13 +191,14 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, mocker):
|
||||
buy=MagicMock(return_value='mocked_limit_buy'),
|
||||
sell=MagicMock(return_value='mocked_limit_sell'))
|
||||
init(default_conf, create_engine('sqlite://'))
|
||||
trade = create_trade(15.0)
|
||||
trade.update(limit_buy_order)
|
||||
Trade.session.add(trade)
|
||||
Trade.session.flush()
|
||||
trade = Trade.query.filter(Trade.is_open.is_(True)).first()
|
||||
create_trade(15.0)
|
||||
|
||||
trade = Trade.query.first()
|
||||
assert trade
|
||||
|
||||
trade.update(limit_buy_order)
|
||||
assert trade.is_open is True
|
||||
|
||||
handle_trade(trade)
|
||||
assert trade.open_order_id == 'mocked_limit_sell'
|
||||
|
||||
@ -204,15 +221,14 @@ def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, mo
|
||||
|
||||
# Create trade and sell it
|
||||
init(default_conf, create_engine('sqlite://'))
|
||||
trade = create_trade(15.0)
|
||||
Trade.session.add(trade)
|
||||
trade.update(limit_buy_order)
|
||||
trade = Trade.query.filter(Trade.is_open.is_(True)).first()
|
||||
create_trade(15.0)
|
||||
|
||||
trade = Trade.query.first()
|
||||
assert trade
|
||||
|
||||
trade.update(limit_buy_order)
|
||||
trade.update(limit_sell_order)
|
||||
trade = Trade.query.filter(Trade.is_open.is_(False)).first()
|
||||
assert trade
|
||||
assert trade.is_open is False
|
||||
|
||||
with pytest.raises(ValueError, match=r'.*closed trade.*'):
|
||||
handle_trade(trade)
|
||||
|
@ -109,6 +109,7 @@ def test_start_backtesting(mocker):
|
||||
live=True,
|
||||
loglevel=20,
|
||||
ticker_interval=1,
|
||||
limit_max_trades=True,
|
||||
)
|
||||
start_backtesting(args)
|
||||
assert env_mock == {
|
||||
@ -116,6 +117,7 @@ def test_start_backtesting(mocker):
|
||||
'BACKTEST_LIVE': 'true',
|
||||
'BACKTEST_CONFIG': 'config.json',
|
||||
'BACKTEST_TICKER_INTERVAL': '1',
|
||||
'BACKTEST_LIMIT_MAX_TRADES': 'true',
|
||||
}
|
||||
assert pytest_mock.call_count == 1
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
# pragma pylint: disable=missing-docstring, too-many-arguments, too-many-ancestors, C0103
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from copy import deepcopy
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from freqtrade.rpc import init, cleanup, send_msg
|
||||
|
||||
|
@ -101,11 +101,7 @@ def test_status_handle(default_conf, update, ticker, mocker):
|
||||
msg_mock.reset_mock()
|
||||
|
||||
# Create some test data
|
||||
trade = create_trade(15.0)
|
||||
assert trade
|
||||
Trade.session.add(trade)
|
||||
Trade.session.flush()
|
||||
|
||||
create_trade(15.0)
|
||||
# Trigger status while we have a fulfilled order for the open trade
|
||||
_status(bot=MagicMock(), update=update)
|
||||
|
||||
@ -141,10 +137,7 @@ def test_status_table_handle(default_conf, update, ticker, mocker):
|
||||
msg_mock.reset_mock()
|
||||
|
||||
# Create some test data
|
||||
trade = create_trade(15.0)
|
||||
assert trade
|
||||
Trade.session.add(trade)
|
||||
Trade.session.flush()
|
||||
create_trade(15.0)
|
||||
|
||||
_status_table(bot=MagicMock(), update=update)
|
||||
|
||||
@ -177,8 +170,8 @@ def test_profit_handle(default_conf, update, ticker, limit_buy_order, limit_sell
|
||||
msg_mock.reset_mock()
|
||||
|
||||
# Create some test data
|
||||
trade = create_trade(15.0)
|
||||
assert trade
|
||||
create_trade(15.0)
|
||||
trade = Trade.query.first()
|
||||
|
||||
# Simulate fulfilled LIMIT_BUY order for trade
|
||||
trade.update(limit_buy_order)
|
||||
@ -193,8 +186,6 @@ def test_profit_handle(default_conf, update, ticker, limit_buy_order, limit_sell
|
||||
|
||||
trade.close_date = datetime.utcnow()
|
||||
trade.is_open = False
|
||||
Trade.session.add(trade)
|
||||
Trade.session.flush()
|
||||
|
||||
_profit(bot=MagicMock(), update=update)
|
||||
assert msg_mock.call_count == 1
|
||||
@ -216,11 +207,10 @@ def test_forcesell_handle(default_conf, update, ticker, mocker):
|
||||
init(default_conf, create_engine('sqlite://'))
|
||||
|
||||
# Create some test data
|
||||
trade = create_trade(15.0)
|
||||
assert trade
|
||||
create_trade(15.0)
|
||||
|
||||
Trade.session.add(trade)
|
||||
Trade.session.flush()
|
||||
trade = Trade.query.first()
|
||||
assert trade
|
||||
|
||||
update.message.text = '/forcesell 1'
|
||||
_forcesell(bot=MagicMock(), update=update)
|
||||
@ -245,8 +235,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, mocker):
|
||||
|
||||
# Create some test data
|
||||
for _ in range(4):
|
||||
Trade.session.add(create_trade(15.0))
|
||||
Trade.session.flush()
|
||||
create_trade(15.0)
|
||||
rpc_mock.reset_mock()
|
||||
|
||||
update.message.text = '/forcesell all'
|
||||
@ -309,7 +298,8 @@ def test_performance_handle(
|
||||
init(default_conf, create_engine('sqlite://'))
|
||||
|
||||
# Create some test data
|
||||
trade = create_trade(15.0)
|
||||
create_trade(15.0)
|
||||
trade = Trade.query.first()
|
||||
assert trade
|
||||
|
||||
# Simulate fulfilled LIMIT_BUY order for trade
|
||||
@ -320,8 +310,6 @@ def test_performance_handle(
|
||||
|
||||
trade.close_date = datetime.utcnow()
|
||||
trade.is_open = False
|
||||
Trade.session.add(trade)
|
||||
Trade.session.flush()
|
||||
|
||||
_performance(bot=MagicMock(), update=update)
|
||||
assert msg_mock.call_count == 1
|
||||
@ -351,9 +339,7 @@ def test_count_handle(default_conf, update, ticker, mocker):
|
||||
update_state(State.RUNNING)
|
||||
|
||||
# Create some test data
|
||||
Trade.session.add(create_trade(15.0))
|
||||
Trade.session.flush()
|
||||
|
||||
create_trade(15.0)
|
||||
msg_mock.reset_mock()
|
||||
_count(bot=MagicMock(), update=update)
|
||||
|
||||
|
8
freqtrade/vendor/qtpylib/indicators.py
vendored
8
freqtrade/vendor/qtpylib/indicators.py
vendored
@ -19,12 +19,12 @@
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
import sys
|
||||
import warnings
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import warnings
|
||||
import sys
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from pandas.core.base import PandasObject
|
||||
|
||||
# =============================================
|
||||
|
@ -18,6 +18,9 @@ def plot_analyzed_dataframe(pair: str) -> None:
|
||||
exchange._API = exchange.Bittrex({'key': '', 'secret': ''})
|
||||
dataframe = analyze.analyze_ticker(pair)
|
||||
|
||||
dataframe.loc[dataframe['buy'] == 1, 'buy_price'] = dataframe['close']
|
||||
dataframe.loc[dataframe['sell'] == 1, 'sell_price'] = dataframe['close']
|
||||
|
||||
# Two subplots sharing x axis
|
||||
fig, (ax1, ax2, ax3) = plt.subplots(3, sharex=True)
|
||||
fig.suptitle(pair, fontsize=14, fontweight='bold')
|
||||
|
Loading…
Reference in New Issue
Block a user