Merge branch 'develop' into arrow_deprecation_timestamp

This commit is contained in:
Matthias 2020-10-20 20:01:54 +02:00
commit 7a092271c5
31 changed files with 291 additions and 214 deletions

View File

@ -140,6 +140,10 @@ information about the bot, we encourage you to join our slack channel.
- [Click here to join Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE). - [Click here to join Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE).
To those interested to check out the newly created discord channel. Click [here](https://discord.gg/MA9v74M)
P.S. currently since discord channel is relatively new, answers to questions might be slightly delayed as currently the user base quite small.
### [Bugs / Issues](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue) ### [Bugs / Issues](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue)
If you discover a bug in the bot, please If you discover a bug in the bot, please

View File

@ -5,15 +5,15 @@
"tradable_balance_ratio": 0.99, "tradable_balance_ratio": 0.99,
"fiat_display_currency": "USD", "fiat_display_currency": "USD",
"timeframe": "5m", "timeframe": "5m",
"dry_run": false, "dry_run": true,
"cancel_open_orders_on_exit": false, "cancel_open_orders_on_exit": false,
"unfilledtimeout": { "unfilledtimeout": {
"buy": 10, "buy": 10,
"sell": 30 "sell": 30
}, },
"bid_strategy": { "bid_strategy": {
"ask_last_balance": 0.0,
"use_order_book": false, "use_order_book": false,
"ask_last_balance": 0.0,
"order_book_top": 1, "order_book_top": 1,
"check_depth_of_market": { "check_depth_of_market": {
"enabled": false, "enabled": false,

View File

@ -7,7 +7,7 @@
"amount_reserve_percent": 0.05, "amount_reserve_percent": 0.05,
"amend_last_stake_amount": false, "amend_last_stake_amount": false,
"last_stake_amount_min_ratio": 0.5, "last_stake_amount_min_ratio": 0.5,
"dry_run": false, "dry_run": true,
"cancel_open_orders_on_exit": false, "cancel_open_orders_on_exit": false,
"timeframe": "5m", "timeframe": "5m",
"trailing_stop": false, "trailing_stop": false,

View File

@ -27,12 +27,11 @@
"use_sell_signal": true, "use_sell_signal": true,
"sell_profit_only": false, "sell_profit_only": false,
"ignore_roi_if_buy_signal": false "ignore_roi_if_buy_signal": false
}, },
"exchange": { "exchange": {
"name": "kraken", "name": "kraken",
"key": "", "key": "your_exchange_key",
"secret": "", "secret": "your_exchange_key",
"ccxt_config": {"enableRateLimit": true}, "ccxt_config": {"enableRateLimit": true},
"ccxt_async_config": { "ccxt_async_config": {
"enableRateLimit": true, "enableRateLimit": true,

View File

@ -64,6 +64,10 @@ For any questions not covered by the documentation or for further information ab
Click [here](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE) to join the Freqtrade Slack channel. Click [here](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE) to join the Freqtrade Slack channel.
To those interested to check out the newly created discord channel. Click [here](https://discord.gg/MA9v74M)
P.S. currently since discord channel is relatively new, answers to questions might be slightly delayed as currently the user base quite small.
## Ready to try? ## Ready to try?
Begin by reading our installation guide [for docker](docker.md), or for [installation without docker](installation.md). Begin by reading our installation guide [for docker](docker.md), or for [installation without docker](installation.md).

View File

@ -1,3 +1,3 @@
mkdocs-material==6.0.2 mkdocs-material==6.1.0
mdx_truly_sane_lists==1.2 mdx_truly_sane_lists==1.2
pymdown-extensions==8.0.1 pymdown-extensions==8.0.1

View File

@ -205,14 +205,14 @@ def start_show_trades(args: Dict[str, Any]) -> None:
""" """
import json import json
from freqtrade.persistence import Trade, init from freqtrade.persistence import Trade, init_db
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
if 'db_url' not in config: if 'db_url' not in config:
raise OperationalException("--db-url is required for this command.") raise OperationalException("--db-url is required for this command.")
logger.info(f'Using DB: "{config["db_url"]}"') logger.info(f'Using DB: "{config["db_url"]}"')
init(config['db_url'], clean_open_orders=False) init_db(config['db_url'], clean_open_orders=False)
tfilter = [] tfilter = []
if config.get('trade_ids'): if config.get('trade_ids'):

View File

@ -9,10 +9,9 @@ from typing import Any, Dict, Optional, Tuple, Union
import numpy as np import numpy as np
import pandas as pd import pandas as pd
from freqtrade import persistence
from freqtrade.constants import LAST_BT_RESULT_FN from freqtrade.constants import LAST_BT_RESULT_FN
from freqtrade.misc import json_load from freqtrade.misc import json_load
from freqtrade.persistence import Trade from freqtrade.persistence import Trade, init_db
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -218,7 +217,7 @@ def load_trades_from_db(db_url: str, strategy: Optional[str] = None) -> pd.DataF
Can also serve as protection to load the correct result. Can also serve as protection to load the correct result.
:return: Dataframe containing Trades :return: Dataframe containing Trades
""" """
persistence.init(db_url, clean_open_orders=False) init_db(db_url, clean_open_orders=False)
columns = ["pair", "open_date", "close_date", "profit", "profit_percent", columns = ["pair", "open_date", "close_date", "profit", "profit_percent",
"open_rate", "close_rate", "amount", "trade_duration", "sell_reason", "open_rate", "close_rate", "amount", "trade_duration", "sell_reason",

View File

@ -5,6 +5,7 @@ from freqtrade.exchange.exchange import Exchange
# isort: on # isort: on
from freqtrade.exchange.bibox import Bibox from freqtrade.exchange.bibox import Bibox
from freqtrade.exchange.binance import Binance from freqtrade.exchange.binance import Binance
from freqtrade.exchange.bittrex import Bittrex
from freqtrade.exchange.exchange import (available_exchanges, ccxt_exchanges, from freqtrade.exchange.exchange import (available_exchanges, ccxt_exchanges,
get_exchange_bad_reason, is_exchange_bad, get_exchange_bad_reason, is_exchange_bad,
is_exchange_known_ccxt, is_exchange_officially_supported, is_exchange_known_ccxt, is_exchange_officially_supported,

View File

@ -20,20 +20,9 @@ class Binance(Exchange):
"order_time_in_force": ['gtc', 'fok', 'ioc'], "order_time_in_force": ['gtc', 'fok', 'ioc'],
"trades_pagination": "id", "trades_pagination": "id",
"trades_pagination_arg": "fromId", "trades_pagination_arg": "fromId",
"l2_limit_range": [5, 10, 20, 50, 100, 500, 1000],
} }
def fetch_l2_order_book(self, pair: str, limit: int = 100) -> dict:
"""
get order book level 2 from exchange
20180619: binance support limits but only on specific range
"""
limit_range = [5, 10, 20, 50, 100, 500, 1000]
# get next-higher step in the limit_range list
limit = min(list(filter(lambda x: limit <= x, limit_range)))
return super().fetch_l2_order_book(pair, limit)
def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool: def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool:
""" """
Verify stop_loss against stoploss-order value (limit or price) Verify stop_loss against stoploss-order value (limit or price)

View File

@ -0,0 +1,23 @@
""" Bittrex exchange subclass """
import logging
from typing import Dict
from freqtrade.exchange import Exchange
logger = logging.getLogger(__name__)
class Bittrex(Exchange):
"""
Bittrex exchange class. Contains adjustments needed for Freqtrade to work
with this exchange.
Please note that this exchange is not included in the list of exchanges
officially supported by the Freqtrade development team. So some features
may still not work as expected.
"""
_ft_has: Dict = {
"l2_limit_range": [1, 25, 500],
}

View File

@ -53,7 +53,7 @@ class Exchange:
"ohlcv_partial_candle": True, "ohlcv_partial_candle": True,
"trades_pagination": "time", # Possible are "time" or "id" "trades_pagination": "time", # Possible are "time" or "id"
"trades_pagination_arg": "since", "trades_pagination_arg": "since",
"l2_limit_range": None,
} }
_ft_has: Dict = {} _ft_has: Dict = {}
@ -1069,6 +1069,16 @@ class Exchange:
return self.fetch_stoploss_order(order_id, pair) return self.fetch_stoploss_order(order_id, pair)
return self.fetch_order(order_id, pair) return self.fetch_order(order_id, pair)
@staticmethod
def get_next_limit_in_list(limit: int, limit_range: Optional[List[int]]):
"""
Get next greater value in the list.
Used by fetch_l2_order_book if the api only supports a limited range
"""
if not limit_range:
return limit
return min([x for x in limit_range if limit <= x] + [max(limit_range)])
@retrier @retrier
def fetch_l2_order_book(self, pair: str, limit: int = 100) -> dict: def fetch_l2_order_book(self, pair: str, limit: int = 100) -> dict:
""" """
@ -1077,9 +1087,10 @@ class Exchange:
Returns a dict in the format Returns a dict in the format
{'asks': [price, volume], 'bids': [price, volume]} {'asks': [price, volume], 'bids': [price, volume]}
""" """
limit1 = self.get_next_limit_in_list(limit, self._ft_has['l2_limit_range'])
try: try:
return self._api.fetch_l2_order_book(pair, limit) return self._api.fetch_l2_order_book(pair, limit1)
except ccxt.NotSupported as e: except ccxt.NotSupported as e:
raise OperationalException( raise OperationalException(
f'Exchange {self._api.name} does not support fetching order book.' f'Exchange {self._api.name} does not support fetching order book.'

View File

@ -12,7 +12,7 @@ from typing import Any, Dict, List, Optional
import arrow import arrow
from cachetools import TTLCache from cachetools import TTLCache
from freqtrade import __version__, constants, persistence from freqtrade import __version__, constants
from freqtrade.configuration import validate_config_consistency from freqtrade.configuration import validate_config_consistency
from freqtrade.data.converter import order_book_to_dataframe from freqtrade.data.converter import order_book_to_dataframe
from freqtrade.data.dataprovider import DataProvider from freqtrade.data.dataprovider import DataProvider
@ -22,7 +22,7 @@ from freqtrade.exceptions import (DependencyException, ExchangeError, Insufficie
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date
from freqtrade.misc import safe_value_fallback, safe_value_fallback2 from freqtrade.misc import safe_value_fallback, safe_value_fallback2
from freqtrade.pairlist.pairlistmanager import PairListManager from freqtrade.pairlist.pairlistmanager import PairListManager
from freqtrade.persistence import Order, Trade from freqtrade.persistence import Order, Trade, cleanup_db, init_db
from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.resolvers import ExchangeResolver, StrategyResolver
from freqtrade.rpc import RPCManager, RPCMessageType from freqtrade.rpc import RPCManager, RPCMessageType
from freqtrade.state import State from freqtrade.state import State
@ -68,7 +68,7 @@ class FreqtradeBot:
self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config) self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config)
persistence.init(self.config.get('db_url', None), clean_open_orders=self.config['dry_run']) init_db(self.config.get('db_url', None), clean_open_orders=self.config['dry_run'])
self.wallets = Wallets(self.config, self.exchange) self.wallets = Wallets(self.config, self.exchange)
@ -123,7 +123,7 @@ class FreqtradeBot:
self.check_for_open_trades() self.check_for_open_trades()
self.rpc.cleanup() self.rpc.cleanup()
persistence.cleanup() cleanup_db()
def startup(self) -> None: def startup(self) -> None:
""" """

View File

@ -4,11 +4,11 @@
This module contains the backtesting logic This module contains the backtesting logic
""" """
import logging import logging
from collections import defaultdict
from copy import deepcopy from copy import deepcopy
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Any, Dict, List, NamedTuple, Optional, Tuple from typing import Any, Dict, List, NamedTuple, Optional, Tuple
import arrow
from pandas import DataFrame from pandas import DataFrame
from freqtrade.configuration import TimeRange, remove_credentials, validate_config_consistency from freqtrade.configuration import TimeRange, remove_credentials, validate_config_consistency
@ -28,6 +28,15 @@ from freqtrade.strategy.interface import IStrategy, SellCheckTuple, SellType
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Indexes for backtest tuples
DATE_IDX = 0
BUY_IDX = 1
OPEN_IDX = 2
CLOSE_IDX = 3
SELL_IDX = 4
LOW_IDX = 5
HIGH_IDX = 6
class BacktestResult(NamedTuple): class BacktestResult(NamedTuple):
""" """
@ -115,7 +124,7 @@ class Backtesting:
""" """
Load strategy into backtesting Load strategy into backtesting
""" """
self.strategy = strategy self.strategy: IStrategy = strategy
# Set stoploss_on_exchange to false for backtesting, # Set stoploss_on_exchange to false for backtesting,
# since a "perfect" stoploss-sell is assumed anyway # since a "perfect" stoploss-sell is assumed anyway
# And the regular "stoploss" function would not apply to that case # And the regular "stoploss" function would not apply to that case
@ -147,12 +156,14 @@ class Backtesting:
return data, timerange return data, timerange
def _get_ohlcv_as_lists(self, processed: Dict) -> Dict[str, DataFrame]: def _get_ohlcv_as_lists(self, processed: Dict[str, DataFrame]) -> Dict[str, Tuple]:
""" """
Helper function to convert a processed dataframes into lists for performance reasons. Helper function to convert a processed dataframes into lists for performance reasons.
Used by backtest() - so keep this optimized for performance. Used by backtest() - so keep this optimized for performance.
""" """
# Every change to this headers list must evaluate further usages of the resulting tuple
# and eventually change the constants for indexes at the top
headers = ['date', 'buy', 'open', 'close', 'sell', 'low', 'high'] headers = ['date', 'buy', 'open', 'close', 'sell', 'low', 'high']
data: Dict = {} data: Dict = {}
# Create dict with data # Create dict with data
@ -172,10 +183,10 @@ class Backtesting:
# Convert from Pandas to list for performance reasons # Convert from Pandas to list for performance reasons
# (Looping Pandas is slow.) # (Looping Pandas is slow.)
data[pair] = [x for x in df_analyzed.itertuples()] data[pair] = [x for x in df_analyzed.itertuples(index=False, name=None)]
return data return data
def _get_close_rate(self, sell_row, trade: Trade, sell: SellCheckTuple, def _get_close_rate(self, sell_row: Tuple, trade: Trade, sell: SellCheckTuple,
trade_dur: int) -> float: trade_dur: int) -> float:
""" """
Get close rate for backtesting result Get close rate for backtesting result
@ -186,12 +197,12 @@ class Backtesting:
return trade.stop_loss return trade.stop_loss
elif sell.sell_type == (SellType.ROI): elif sell.sell_type == (SellType.ROI):
roi_entry, roi = self.strategy.min_roi_reached_entry(trade_dur) roi_entry, roi = self.strategy.min_roi_reached_entry(trade_dur)
if roi is not None: if roi is not None and roi_entry is not None:
if roi == -1 and roi_entry % self.timeframe_min == 0: if roi == -1 and roi_entry % self.timeframe_min == 0:
# When forceselling with ROI=-1, the roi time will always be equal to trade_dur. # When forceselling with ROI=-1, the roi time will always be equal to trade_dur.
# If that entry is a multiple of the timeframe (so on candle open) # If that entry is a multiple of the timeframe (so on candle open)
# - we'll use open instead of close # - we'll use open instead of close
return sell_row.open return sell_row[OPEN_IDX]
# - (Expected abs profit + open_rate + open_fee) / (fee_close -1) # - (Expected abs profit + open_rate + open_fee) / (fee_close -1)
close_rate = - (trade.open_rate * roi + trade.open_rate * close_rate = - (trade.open_rate * roi + trade.open_rate *
@ -199,57 +210,38 @@ class Backtesting:
if (trade_dur > 0 and trade_dur == roi_entry if (trade_dur > 0 and trade_dur == roi_entry
and roi_entry % self.timeframe_min == 0 and roi_entry % self.timeframe_min == 0
and sell_row.open > close_rate): and sell_row[OPEN_IDX] > close_rate):
# new ROI entry came into effect. # new ROI entry came into effect.
# use Open rate if open_rate > calculated sell rate # use Open rate if open_rate > calculated sell rate
return sell_row.open return sell_row[OPEN_IDX]
# Use the maximum between close_rate and low as we # Use the maximum between close_rate and low as we
# cannot sell outside of a candle. # cannot sell outside of a candle.
# Applies when a new ROI setting comes in place and the whole candle is above that. # Applies when a new ROI setting comes in place and the whole candle is above that.
return max(close_rate, sell_row.low) return max(close_rate, sell_row[LOW_IDX])
else: else:
# This should not be reached... # This should not be reached...
return sell_row.open return sell_row[OPEN_IDX]
else: else:
return sell_row.open return sell_row[OPEN_IDX]
def _get_sell_trade_entry( def _get_sell_trade_entry(self, trade: Trade, sell_row: Tuple) -> Optional[BacktestResult]:
self, pair: str, buy_row: DataFrame,
partial_ohlcv: List, trade_count_lock: Dict,
stake_amount: float, max_open_trades: int) -> Optional[BacktestResult]:
trade = Trade( sell = self.strategy.should_sell(trade, sell_row[OPEN_IDX], sell_row[DATE_IDX],
pair=pair, sell_row[BUY_IDX], sell_row[SELL_IDX],
open_rate=buy_row.open, low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX])
open_date=buy_row.date,
stake_amount=stake_amount,
amount=round(stake_amount / buy_row.open, 8),
fee_open=self.fee,
fee_close=self.fee,
is_open=True,
)
logger.debug(f"{pair} - Backtesting emulates creation of new trade: {trade}.")
# calculate win/lose forwards from buy point
for sell_row in partial_ohlcv:
if max_open_trades > 0:
# Increase trade_count_lock for every iteration
trade_count_lock[sell_row.date] = trade_count_lock.get(sell_row.date, 0) + 1
sell = self.strategy.should_sell(trade, sell_row.open, sell_row.date, sell_row.buy,
sell_row.sell, low=sell_row.low, high=sell_row.high)
if sell.sell_flag: if sell.sell_flag:
trade_dur = int((sell_row.date - buy_row.date).total_seconds() // 60) trade_dur = int((sell_row[DATE_IDX] - trade.open_date).total_seconds() // 60)
closerate = self._get_close_rate(sell_row, trade, sell, trade_dur) closerate = self._get_close_rate(sell_row, trade, sell, trade_dur)
return BacktestResult(pair=pair, return BacktestResult(pair=trade.pair,
profit_percent=trade.calc_profit_ratio(rate=closerate), profit_percent=trade.calc_profit_ratio(rate=closerate),
profit_abs=trade.calc_profit(rate=closerate), profit_abs=trade.calc_profit(rate=closerate),
open_date=buy_row.date, open_date=trade.open_date,
open_rate=buy_row.open, open_rate=trade.open_rate,
open_fee=self.fee, open_fee=self.fee,
close_date=sell_row.date, close_date=sell_row[DATE_IDX],
close_rate=closerate, close_rate=closerate,
close_fee=self.fee, close_fee=self.fee,
amount=trade.amount, amount=trade.amount,
@ -257,33 +249,40 @@ class Backtesting:
open_at_end=False, open_at_end=False,
sell_reason=sell.sell_type sell_reason=sell.sell_type
) )
if partial_ohlcv: return None
# no sell condition found - trade stil open at end of backtest period
sell_row = partial_ohlcv[-1] def handle_left_open(self, open_trades: Dict[str, List[Trade]],
bt_res = BacktestResult(pair=pair, data: Dict[str, List[Tuple]]) -> List[BacktestResult]:
profit_percent=trade.calc_profit_ratio(rate=sell_row.open), """
profit_abs=trade.calc_profit(rate=sell_row.open), Handling of left open trades at the end of backtesting
open_date=buy_row.date, """
open_rate=buy_row.open, trades = []
for pair in open_trades.keys():
if len(open_trades[pair]) > 0:
for trade in open_trades[pair]:
sell_row = data[pair][-1]
trade_entry = BacktestResult(pair=trade.pair,
profit_percent=trade.calc_profit_ratio(
rate=sell_row[OPEN_IDX]),
profit_abs=trade.calc_profit(sell_row[OPEN_IDX]),
open_date=trade.open_date,
open_rate=trade.open_rate,
open_fee=self.fee, open_fee=self.fee,
close_date=sell_row.date, close_date=sell_row[DATE_IDX],
close_rate=sell_row.open, close_rate=sell_row[OPEN_IDX],
close_fee=self.fee, close_fee=self.fee,
amount=trade.amount, amount=trade.amount,
trade_duration=int(( trade_duration=int((
sell_row.date - buy_row.date).total_seconds() // 60), sell_row[DATE_IDX] - trade.open_date
).total_seconds() // 60),
open_at_end=True, open_at_end=True,
sell_reason=SellType.FORCE_SELL sell_reason=SellType.FORCE_SELL
) )
logger.debug(f"{pair} - Force selling still open trade, " trades.append(trade_entry)
f"profit percent: {bt_res.profit_percent}, " return trades
f"profit abs: {bt_res.profit_abs}")
return bt_res
return None
def backtest(self, processed: Dict, stake_amount: float, def backtest(self, processed: Dict, stake_amount: float,
start_date: arrow.Arrow, end_date: arrow.Arrow, start_date: datetime, end_date: datetime,
max_open_trades: int = 0, position_stacking: bool = False) -> DataFrame: max_open_trades: int = 0, position_stacking: bool = False) -> DataFrame:
""" """
Implement backtesting functionality Implement backtesting functionality
@ -305,19 +304,21 @@ class Backtesting:
f"max_open_trades: {max_open_trades}, position_stacking: {position_stacking}" f"max_open_trades: {max_open_trades}, position_stacking: {position_stacking}"
) )
trades = [] trades = []
trade_count_lock: Dict = {}
# Use dict of lists with data for performance # Use dict of lists with data for performance
# (looping lists is a lot faster than pandas DataFrames) # (looping lists is a lot faster than pandas DataFrames)
data: Dict = self._get_ohlcv_as_lists(processed) data: Dict = self._get_ohlcv_as_lists(processed)
lock_pair_until: Dict = {}
# Indexes per pair, so some pairs are allowed to have a missing start. # Indexes per pair, so some pairs are allowed to have a missing start.
indexes: Dict = {} indexes: Dict = {}
tmp = start_date + timedelta(minutes=self.timeframe_min) tmp = start_date + timedelta(minutes=self.timeframe_min)
open_trades: Dict[str, List] = defaultdict(list)
open_trade_count = 0
# Loop timerange and get candle for each pair at that point in time # Loop timerange and get candle for each pair at that point in time
while tmp < end_date: while tmp <= end_date:
open_trade_count_start = open_trade_count
for i, pair in enumerate(data): for i, pair in enumerate(data):
if pair not in indexes: if pair not in indexes:
@ -331,42 +332,52 @@ class Backtesting:
continue continue
# Waits until the time-counter reaches the start of the data for this pair. # Waits until the time-counter reaches the start of the data for this pair.
if row.date > tmp.datetime: if row[DATE_IDX] > tmp:
continue continue
indexes[pair] += 1 indexes[pair] += 1
if row.buy == 0 or row.sell == 1:
continue # skip rows where no buy signal or that would immediately sell off
if (not position_stacking and pair in lock_pair_until
and row.date <= lock_pair_until[pair]):
# without positionstacking, we can only have one open trade per pair. # without positionstacking, we can only have one open trade per pair.
continue # max_open_trades must be respected
# don't open on the last row
if max_open_trades > 0: if ((position_stacking or len(open_trades[pair]) == 0)
# Check if max_open_trades has already been reached for the given date and max_open_trades > 0 and open_trade_count_start < max_open_trades
if not trade_count_lock.get(row.date, 0) < max_open_trades: and tmp != end_date
continue and row[BUY_IDX] == 1 and row[SELL_IDX] != 1):
trade_count_lock[row.date] = trade_count_lock.get(row.date, 0) + 1 # Enter trade
trade = Trade(
pair=pair,
open_rate=row[OPEN_IDX],
open_date=row[DATE_IDX],
stake_amount=stake_amount,
amount=round(stake_amount / row[OPEN_IDX], 8),
fee_open=self.fee,
fee_close=self.fee,
is_open=True,
)
# TODO: hacky workaround to avoid opening > max_open_trades
# This emulates previous behaviour - not sure if this is correct
# Prevents buying if the trade-slot was freed in this candle
open_trade_count_start += 1
open_trade_count += 1
# logger.debug(f"{pair} - Backtesting emulates creation of new trade: {trade}.")
open_trades[pair].append(trade)
for trade in open_trades[pair]:
# since indexes has been incremented before, we need to go one step back to # since indexes has been incremented before, we need to go one step back to
# also check the buying candle for sell conditions. # also check the buying candle for sell conditions.
trade_entry = self._get_sell_trade_entry(pair, row, data[pair][indexes[pair]-1:], trade_entry = self._get_sell_trade_entry(trade, row)
trade_count_lock, stake_amount, # Sell occured
max_open_trades)
if trade_entry: if trade_entry:
logger.debug(f"{pair} - Locking pair till " # logger.debug(f"{pair} - Backtesting sell {trade}")
f"close_date={trade_entry.close_date}") open_trade_count -= 1
lock_pair_until[pair] = trade_entry.close_date open_trades[pair].remove(trade)
trades.append(trade_entry) trades.append(trade_entry)
else:
# Set lock_pair_until to end of testing period if trade could not be closed
lock_pair_until[pair] = end_date.datetime
# Move time one configured time_interval ahead. # Move time one configured time_interval ahead.
tmp += timedelta(minutes=self.timeframe_min) tmp += timedelta(minutes=self.timeframe_min)
trades += self.handle_left_open(open_trades, data=data)
return DataFrame.from_records(trades, columns=BacktestResult._fields) return DataFrame.from_records(trades, columns=BacktestResult._fields)
def start(self) -> None: def start(self) -> None:
@ -412,8 +423,8 @@ class Backtesting:
results = self.backtest( results = self.backtest(
processed=preprocessed, processed=preprocessed,
stake_amount=self.config['stake_amount'], stake_amount=self.config['stake_amount'],
start_date=min_date, start_date=min_date.datetime,
end_date=max_date, end_date=max_date.datetime,
max_open_trades=max_open_trades, max_open_trades=max_open_trades,
position_stacking=position_stacking, position_stacking=position_stacking,
) )

View File

@ -94,14 +94,14 @@ class Hyperopt:
# Populate functions here (hasattr is slow so should not be run during "regular" operations) # Populate functions here (hasattr is slow so should not be run during "regular" operations)
if hasattr(self.custom_hyperopt, 'populate_indicators'): if hasattr(self.custom_hyperopt, 'populate_indicators'):
self.backtesting.strategy.advise_indicators = \ self.backtesting.strategy.advise_indicators = ( # type: ignore
self.custom_hyperopt.populate_indicators # type: ignore self.custom_hyperopt.populate_indicators) # type: ignore
if hasattr(self.custom_hyperopt, 'populate_buy_trend'): if hasattr(self.custom_hyperopt, 'populate_buy_trend'):
self.backtesting.strategy.advise_buy = \ self.backtesting.strategy.advise_buy = ( # type: ignore
self.custom_hyperopt.populate_buy_trend # type: ignore self.custom_hyperopt.populate_buy_trend) # type: ignore
if hasattr(self.custom_hyperopt, 'populate_sell_trend'): if hasattr(self.custom_hyperopt, 'populate_sell_trend'):
self.backtesting.strategy.advise_sell = \ self.backtesting.strategy.advise_sell = ( # type: ignore
self.custom_hyperopt.populate_sell_trend # type: ignore self.custom_hyperopt.populate_sell_trend) # type: ignore
# Use max_open_trades for hyperopt as well, except --disable-max-market-positions is set # Use max_open_trades for hyperopt as well, except --disable-max-market-positions is set
if self.config.get('use_max_market_positions', True): if self.config.get('use_max_market_positions', True):
@ -508,16 +508,16 @@ class Hyperopt:
params_details = self._get_params_details(params_dict) params_details = self._get_params_details(params_dict)
if self.has_space('roi'): if self.has_space('roi'):
self.backtesting.strategy.minimal_roi = \ self.backtesting.strategy.minimal_roi = ( # type: ignore
self.custom_hyperopt.generate_roi_table(params_dict) self.custom_hyperopt.generate_roi_table(params_dict))
if self.has_space('buy'): if self.has_space('buy'):
self.backtesting.strategy.advise_buy = \ self.backtesting.strategy.advise_buy = ( # type: ignore
self.custom_hyperopt.buy_strategy_generator(params_dict) self.custom_hyperopt.buy_strategy_generator(params_dict))
if self.has_space('sell'): if self.has_space('sell'):
self.backtesting.strategy.advise_sell = \ self.backtesting.strategy.advise_sell = ( # type: ignore
self.custom_hyperopt.sell_strategy_generator(params_dict) self.custom_hyperopt.sell_strategy_generator(params_dict))
if self.has_space('stoploss'): if self.has_space('stoploss'):
self.backtesting.strategy.stoploss = params_dict['stoploss'] self.backtesting.strategy.stoploss = params_dict['stoploss']
@ -538,8 +538,8 @@ class Hyperopt:
backtesting_results = self.backtesting.backtest( backtesting_results = self.backtesting.backtest(
processed=processed, processed=processed,
stake_amount=self.config['stake_amount'], stake_amount=self.config['stake_amount'],
start_date=min_date, start_date=min_date.datetime,
end_date=max_date, end_date=max_date.datetime,
max_open_trades=self.max_open_trades, max_open_trades=self.max_open_trades,
position_stacking=self.position_stacking, position_stacking=self.position_stacking,
) )

View File

@ -1,3 +1,3 @@
# flake8: noqa: F401 # flake8: noqa: F401
from freqtrade.persistence.models import Order, Trade, clean_dry_run_db, cleanup, init from freqtrade.persistence.models import Order, Trade, clean_dry_run_db, cleanup_db, init_db

View File

@ -29,7 +29,7 @@ _DECL_BASE: Any = declarative_base()
_SQL_DOCS_URL = 'http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls' _SQL_DOCS_URL = 'http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls'
def init(db_url: str, clean_open_orders: bool = False) -> None: def init_db(db_url: str, clean_open_orders: bool = False) -> None:
""" """
Initializes this module with the given config, Initializes this module with the given config,
registers all known command handlers registers all known command handlers
@ -72,7 +72,7 @@ def init(db_url: str, clean_open_orders: bool = False) -> None:
clean_dry_run_db() clean_dry_run_db()
def cleanup() -> None: def cleanup_db() -> None:
""" """
Flushes all pending operations to disk. Flushes all pending operations to disk.
:return: None :return: None
@ -167,12 +167,12 @@ class Order(_DECL_BASE):
""" """
Get all non-closed orders - useful when trying to batch-update orders Get all non-closed orders - useful when trying to batch-update orders
""" """
filtered_orders = [o for o in orders if o.order_id == order['id']] filtered_orders = [o for o in orders if o.order_id == order.get('id')]
if filtered_orders: if filtered_orders:
oobj = filtered_orders[0] oobj = filtered_orders[0]
oobj.update_from_ccxt_object(order) oobj.update_from_ccxt_object(order)
else: else:
logger.warning(f"Did not find order for {order['id']}.") logger.warning(f"Did not find order for {order}.")
@staticmethod @staticmethod
def parse_from_ccxt_object(order: Dict[str, Any], pair: str, side: str) -> 'Order': def parse_from_ccxt_object(order: Dict[str, Any], pair: str, side: str) -> 'Order':
@ -399,7 +399,7 @@ class Trade(_DECL_BASE):
self.close(order['average']) self.close(order['average'])
else: else:
raise ValueError(f'Unknown order type: {order_type}') raise ValueError(f'Unknown order type: {order_type}')
cleanup() cleanup_db()
def close(self, rate: float) -> None: def close(self, rate: float) -> None:
""" """

View File

@ -563,7 +563,7 @@ class ApiServer(RPC):
config.update({ config.update({
'strategy': strategy, 'strategy': strategy,
}) })
results = self._rpc_analysed_history_full(config, pair, timeframe, timerange) results = RPC._rpc_analysed_history_full(config, pair, timeframe, timerange)
return jsonify(results) return jsonify(results)
@require_login @require_login

View File

@ -656,8 +656,9 @@ class RPC:
raise RPCException('Edge is not enabled.') raise RPCException('Edge is not enabled.')
return self._freqtrade.edge.accepted_pairs() return self._freqtrade.edge.accepted_pairs()
def _convert_dataframe_to_dict(self, strategy: str, pair: str, timeframe: str, @staticmethod
dataframe: DataFrame, last_analyzed: datetime) -> Dict[str, Any]: def _convert_dataframe_to_dict(strategy: str, pair: str, timeframe: str, dataframe: DataFrame,
last_analyzed: datetime) -> Dict[str, Any]:
has_content = len(dataframe) != 0 has_content = len(dataframe) != 0
buy_signals = 0 buy_signals = 0
sell_signals = 0 sell_signals = 0
@ -711,7 +712,8 @@ class RPC:
return self._convert_dataframe_to_dict(self._freqtrade.config['strategy'], return self._convert_dataframe_to_dict(self._freqtrade.config['strategy'],
pair, timeframe, _data, last_analyzed) pair, timeframe, _data, last_analyzed)
def _rpc_analysed_history_full(self, config, pair: str, timeframe: str, @staticmethod
def _rpc_analysed_history_full(config, pair: str, timeframe: str,
timerange: str) -> Dict[str, Any]: timerange: str) -> Dict[str, Any]:
timerange_parsed = TimeRange.parse_timerange(timerange) timerange_parsed = TimeRange.parse_timerange(timerange)
@ -726,7 +728,7 @@ class RPC:
strategy = StrategyResolver.load_strategy(config) strategy = StrategyResolver.load_strategy(config)
df_analyzed = strategy.analyze_ticker(_data[pair], {'pair': pair}) df_analyzed = strategy.analyze_ticker(_data[pair], {'pair': pair})
return self._convert_dataframe_to_dict(strategy.get_strategy_name(), pair, timeframe, return RPC._convert_dataframe_to_dict(strategy.get_strategy_name(), pair, timeframe,
df_analyzed, arrow.Arrow.utcnow().datetime) df_analyzed, arrow.Arrow.utcnow().datetime)
def _rpc_plot_config(self) -> Dict[str, Any]: def _rpc_plot_config(self) -> Dict[str, Any]:

View File

@ -13,7 +13,7 @@ pytest-asyncio==0.14.0
pytest-cov==2.10.1 pytest-cov==2.10.1
pytest-mock==3.3.1 pytest-mock==3.3.1
pytest-random-order==1.0.4 pytest-random-order==1.0.4
isort==5.6.3 isort==5.6.4
# Convert jupyter notebooks to markdown documents # Convert jupyter notebooks to markdown documents
nbconvert==6.0.7 nbconvert==6.0.7

View File

@ -2,7 +2,7 @@
-r requirements.txt -r requirements.txt
# Required for hyperopt # Required for hyperopt
scipy==1.5.2 scipy==1.5.3
scikit-learn==0.23.2 scikit-learn==0.23.2
scikit-optimize==0.8.1 scikit-optimize==0.8.1
filelock==3.0.12 filelock==3.0.12

View File

@ -1,11 +1,11 @@
numpy==1.19.2 numpy==1.19.2
pandas==1.1.3 pandas==1.1.3
ccxt==1.36.2 ccxt==1.36.66
multidict==4.7.6 multidict==4.7.6
aiohttp==3.6.3 aiohttp==3.6.3
SQLAlchemy==1.3.19 SQLAlchemy==1.3.20
python-telegram-bot==12.8 python-telegram-bot==13.0
arrow==0.17.0 arrow==0.17.0
cachetools==4.1.1 cachetools==4.1.1
requests==2.24.0 requests==2.24.0
@ -34,7 +34,7 @@ flask-jwt-extended==3.24.1
flask-cors==3.0.9 flask-cors==3.0.9
# Support for colorized terminal output # Support for colorized terminal output
colorama==0.4.3 colorama==0.4.4
# Building config files interactively # Building config files interactively
questionary==1.6.0 questionary==1.7.0
prompt-toolkit==3.0.7 prompt-toolkit==3.0.8

View File

@ -1150,7 +1150,7 @@ def test_start_list_data(testdatadir, capsys):
@pytest.mark.usefixtures("init_persistence") @pytest.mark.usefixtures("init_persistence")
def test_show_trades(mocker, fee, capsys, caplog): def test_show_trades(mocker, fee, capsys, caplog):
mocker.patch("freqtrade.persistence.init") mocker.patch("freqtrade.persistence.init_db")
create_mock_trades(fee) create_mock_trades(fee)
args = [ args = [
"show-trades", "show-trades",

View File

@ -13,13 +13,13 @@ import numpy as np
import pytest import pytest
from telegram import Chat, Message, Update from telegram import Chat, Message, Update
from freqtrade import constants, persistence from freqtrade import constants
from freqtrade.commands import Arguments from freqtrade.commands import Arguments
from freqtrade.data.converter import ohlcv_to_dataframe from freqtrade.data.converter import ohlcv_to_dataframe
from freqtrade.edge import Edge, PairInfo from freqtrade.edge import Edge, PairInfo
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
from freqtrade.freqtradebot import FreqtradeBot from freqtrade.freqtradebot import FreqtradeBot
from freqtrade.persistence import Trade from freqtrade.persistence import Trade, init_db
from freqtrade.resolvers import ExchangeResolver from freqtrade.resolvers import ExchangeResolver
from freqtrade.worker import Worker from freqtrade.worker import Worker
from tests.conftest_trades import (mock_trade_1, mock_trade_2, mock_trade_3, mock_trade_4, from tests.conftest_trades import (mock_trade_1, mock_trade_2, mock_trade_3, mock_trade_4,
@ -131,7 +131,7 @@ def patch_freqtradebot(mocker, config) -> None:
:return: None :return: None
""" """
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
persistence.init(config['db_url']) init_db(config['db_url'])
patch_exchange(mocker) patch_exchange(mocker)
mocker.patch('freqtrade.freqtradebot.RPCManager._init', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager._init', MagicMock())
mocker.patch('freqtrade.freqtradebot.RPCManager.send_msg', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager.send_msg', MagicMock())
@ -219,7 +219,7 @@ def patch_coingekko(mocker) -> None:
@pytest.fixture(scope='function') @pytest.fixture(scope='function')
def init_persistence(default_conf): def init_persistence(default_conf):
persistence.init(default_conf['db_url'], default_conf['dry_run']) init_db(default_conf['db_url'], default_conf['dry_run'])
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
@ -297,7 +297,7 @@ def default_conf(testdatadir):
@pytest.fixture @pytest.fixture
def update(): def update():
_update = Update(0) _update = Update(0)
_update.message = Message(0, 0, datetime.utcnow(), Chat(0, 0)) _update.message = Message(0, datetime.utcnow(), Chat(0, 0))
return _update return _update

View File

@ -114,7 +114,7 @@ def test_load_trades_from_db(default_conf, fee, mocker):
create_mock_trades(fee) create_mock_trades(fee)
# remove init so it does not init again # remove init so it does not init again
init_mock = mocker.patch('freqtrade.persistence.init', MagicMock()) init_mock = mocker.patch('freqtrade.data.btanalysis.init_db', MagicMock())
trades = load_trades_from_db(db_url=default_conf['db_url']) trades = load_trades_from_db(db_url=default_conf['db_url'])
assert init_mock.call_count == 1 assert init_mock.call_count == 1

View File

@ -132,7 +132,7 @@ def test_orderbook(mocker, default_conf, order_book_l2):
res = dp.orderbook('ETH/BTC', 5) res = dp.orderbook('ETH/BTC', 5)
assert order_book_l2.call_count == 1 assert order_book_l2.call_count == 1
assert order_book_l2.call_args_list[0][0][0] == 'ETH/BTC' assert order_book_l2.call_args_list[0][0][0] == 'ETH/BTC'
assert order_book_l2.call_args_list[0][0][1] == 5 assert order_book_l2.call_args_list[0][0][1] >= 5
assert type(res) is dict assert type(res) is dict
assert 'bids' in res assert 'bids' in res

View File

@ -11,7 +11,7 @@ from pandas import DataFrame
from freqtrade.exceptions import (DDosProtection, DependencyException, InvalidOrderException, from freqtrade.exceptions import (DDosProtection, DependencyException, InvalidOrderException,
OperationalException, TemporaryError) OperationalException, TemporaryError)
from freqtrade.exchange import Binance, Exchange, Kraken from freqtrade.exchange import Binance, Bittrex, Exchange, Kraken
from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, API_RETRY_COUNT, from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, API_RETRY_COUNT,
calculate_backoff) calculate_backoff)
from freqtrade.exchange.exchange import (market_is_active, timeframe_to_minutes, timeframe_to_msecs, from freqtrade.exchange.exchange import (market_is_active, timeframe_to_minutes, timeframe_to_msecs,
@ -148,11 +148,19 @@ def test_exchange_resolver(default_conf, mocker, caplog):
mocker.patch('freqtrade.exchange.Exchange.validate_pairs') mocker.patch('freqtrade.exchange.Exchange.validate_pairs')
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
exchange = ExchangeResolver.load_exchange('Bittrex', default_conf)
exchange = ExchangeResolver.load_exchange('huobi', default_conf)
assert isinstance(exchange, Exchange) assert isinstance(exchange, Exchange)
assert log_has_re(r"No .* specific subclass found. Using the generic class instead.", caplog) assert log_has_re(r"No .* specific subclass found. Using the generic class instead.", caplog)
caplog.clear() caplog.clear()
exchange = ExchangeResolver.load_exchange('Bittrex', default_conf)
assert isinstance(exchange, Exchange)
assert isinstance(exchange, Bittrex)
assert not log_has_re(r"No .* specific subclass found. Using the generic class instead.",
caplog)
caplog.clear()
exchange = ExchangeResolver.load_exchange('kraken', default_conf) exchange = ExchangeResolver.load_exchange('kraken', default_conf)
assert isinstance(exchange, Exchange) assert isinstance(exchange, Exchange)
assert isinstance(exchange, Kraken) assert isinstance(exchange, Kraken)
@ -1439,6 +1447,27 @@ def test_refresh_latest_ohlcv_inv_result(default_conf, mocker, caplog):
assert log_has("Async code raised an exception: TypeError", caplog) assert log_has("Async code raised an exception: TypeError", caplog)
def test_get_next_limit_in_list():
limit_range = [5, 10, 20, 50, 100, 500, 1000]
assert Exchange.get_next_limit_in_list(1, limit_range) == 5
assert Exchange.get_next_limit_in_list(5, limit_range) == 5
assert Exchange.get_next_limit_in_list(6, limit_range) == 10
assert Exchange.get_next_limit_in_list(9, limit_range) == 10
assert Exchange.get_next_limit_in_list(10, limit_range) == 10
assert Exchange.get_next_limit_in_list(11, limit_range) == 20
assert Exchange.get_next_limit_in_list(19, limit_range) == 20
assert Exchange.get_next_limit_in_list(21, limit_range) == 50
assert Exchange.get_next_limit_in_list(51, limit_range) == 100
assert Exchange.get_next_limit_in_list(1000, limit_range) == 1000
# Going over the limit ...
assert Exchange.get_next_limit_in_list(1001, limit_range) == 1000
assert Exchange.get_next_limit_in_list(2000, limit_range) == 1000
assert Exchange.get_next_limit_in_list(21, None) == 21
assert Exchange.get_next_limit_in_list(100, None) == 100
assert Exchange.get_next_limit_in_list(1000, None) == 1000
@pytest.mark.parametrize("exchange_name", EXCHANGES) @pytest.mark.parametrize("exchange_name", EXCHANGES)
def test_fetch_l2_order_book(default_conf, mocker, order_book_l2, exchange_name): def test_fetch_l2_order_book(default_conf, mocker, order_book_l2, exchange_name):
default_conf['exchange']['name'] = exchange_name default_conf['exchange']['name'] = exchange_name
@ -1451,6 +1480,19 @@ def test_fetch_l2_order_book(default_conf, mocker, order_book_l2, exchange_name)
assert 'asks' in order_book assert 'asks' in order_book
assert len(order_book['bids']) == 10 assert len(order_book['bids']) == 10
assert len(order_book['asks']) == 10 assert len(order_book['asks']) == 10
assert api_mock.fetch_l2_order_book.call_args_list[0][0][0] == 'ETH/BTC'
for val in [1, 5, 10, 12, 20, 50, 100]:
api_mock.fetch_l2_order_book.reset_mock()
order_book = exchange.fetch_l2_order_book(pair='ETH/BTC', limit=val)
assert api_mock.fetch_l2_order_book.call_args_list[0][0][0] == 'ETH/BTC'
# Not all exchanges support all limits for orderbook
if not exchange._ft_has['l2_limit_range'] or val in exchange._ft_has['l2_limit_range']:
assert api_mock.fetch_l2_order_book.call_args_list[0][0][1] == val
else:
next_limit = exchange.get_next_limit_in_list(val, exchange._ft_has['l2_limit_range'])
assert api_mock.fetch_l2_order_book.call_args_list[0][0][1] == next_limit
@pytest.mark.parametrize("exchange_name", EXCHANGES) @pytest.mark.parametrize("exchange_name", EXCHANGES)

View File

@ -82,7 +82,7 @@ def test_telegram_init(default_conf, mocker, caplog) -> None:
assert log_has(message_str, caplog) assert log_has(message_str, caplog)
def test_cleanup(default_conf, mocker) -> None: def test_cleanup(default_conf, mocker, ) -> None:
updater_mock = MagicMock() updater_mock = MagicMock()
updater_mock.stop = MagicMock() updater_mock.stop = MagicMock()
mocker.patch('freqtrade.rpc.telegram.Updater', updater_mock) mocker.patch('freqtrade.rpc.telegram.Updater', updater_mock)
@ -92,13 +92,9 @@ def test_cleanup(default_conf, mocker) -> None:
assert telegram._updater.stop.call_count == 1 assert telegram._updater.stop.call_count == 1
def test_authorized_only(default_conf, mocker, caplog) -> None: def test_authorized_only(default_conf, mocker, caplog, update) -> None:
patch_exchange(mocker) patch_exchange(mocker)
chat = Chat(0, 0)
update = Update(randint(1, 100))
update.message = Message(randint(1, 100), 0, datetime.utcnow(), chat)
default_conf['telegram']['enabled'] = False default_conf['telegram']['enabled'] = False
bot = FreqtradeBot(default_conf) bot = FreqtradeBot(default_conf)
patch_get_signal(bot, (True, False)) patch_get_signal(bot, (True, False))
@ -114,7 +110,7 @@ def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None:
patch_exchange(mocker) patch_exchange(mocker)
chat = Chat(0xdeadbeef, 0) chat = Chat(0xdeadbeef, 0)
update = Update(randint(1, 100)) update = Update(randint(1, 100))
update.message = Message(randint(1, 100), 0, datetime.utcnow(), chat) update.message = Message(randint(1, 100), datetime.utcnow(), chat)
default_conf['telegram']['enabled'] = False default_conf['telegram']['enabled'] = False
bot = FreqtradeBot(default_conf) bot = FreqtradeBot(default_conf)
@ -127,12 +123,9 @@ def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None:
assert not log_has('Exception occurred within Telegram module', caplog) assert not log_has('Exception occurred within Telegram module', caplog)
def test_authorized_only_exception(default_conf, mocker, caplog) -> None: def test_authorized_only_exception(default_conf, mocker, caplog, update) -> None:
patch_exchange(mocker) patch_exchange(mocker)
update = Update(randint(1, 100))
update.message = Message(randint(1, 100), 0, datetime.utcnow(), Chat(0, 0))
default_conf['telegram']['enabled'] = False default_conf['telegram']['enabled'] = False
bot = FreqtradeBot(default_conf) bot = FreqtradeBot(default_conf)
@ -146,7 +139,7 @@ def test_authorized_only_exception(default_conf, mocker, caplog) -> None:
assert log_has('Exception occurred within Telegram module', caplog) assert log_has('Exception occurred within Telegram module', caplog)
def test_telegram_status(default_conf, update, mocker, fee, ticker,) -> None: def test_telegram_status(default_conf, update, mocker) -> None:
update.message.chat.id = "123" update.message.chat.id = "123"
default_conf['telegram']['enabled'] = False default_conf['telegram']['enabled'] = False
default_conf['telegram']['chat_id'] = "123" default_conf['telegram']['chat_id'] = "123"

View File

@ -15,8 +15,7 @@ from freqtrade.exceptions import (DependencyException, ExchangeError, Insufficie
InvalidOrderException, OperationalException, PricingError, InvalidOrderException, OperationalException, PricingError,
TemporaryError) TemporaryError)
from freqtrade.freqtradebot import FreqtradeBot from freqtrade.freqtradebot import FreqtradeBot
from freqtrade.persistence import Trade from freqtrade.persistence import Order, Trade
from freqtrade.persistence.models import Order
from freqtrade.rpc import RPCMessageType from freqtrade.rpc import RPCMessageType
from freqtrade.state import RunMode, State from freqtrade.state import RunMode, State
from freqtrade.strategy.interface import SellCheckTuple, SellType from freqtrade.strategy.interface import SellCheckTuple, SellType
@ -66,7 +65,7 @@ def test_process_stopped(mocker, default_conf) -> None:
def test_bot_cleanup(mocker, default_conf, caplog) -> None: def test_bot_cleanup(mocker, default_conf, caplog) -> None:
mock_cleanup = mocker.patch('freqtrade.persistence.cleanup') mock_cleanup = mocker.patch('freqtrade.freqtradebot.cleanup_db')
coo_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.cancel_all_open_orders') coo_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.cancel_all_open_orders')
freqtrade = get_patched_freqtradebot(mocker, default_conf) freqtrade = get_patched_freqtradebot(mocker, default_conf)
freqtrade.cleanup() freqtrade.cleanup()

View File

@ -65,7 +65,7 @@ def test_main_fatal_exception(mocker, default_conf, caplog) -> None:
mocker.patch('freqtrade.worker.Worker._worker', MagicMock(side_effect=Exception)) mocker.patch('freqtrade.worker.Worker._worker', MagicMock(side_effect=Exception))
patched_configuration_load_config_file(mocker, default_conf) patched_configuration_load_config_file(mocker, default_conf)
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) mocker.patch('freqtrade.freqtradebot.init_db', MagicMock())
args = ['trade', '-c', 'config.json.example'] args = ['trade', '-c', 'config.json.example']
@ -83,7 +83,7 @@ def test_main_keyboard_interrupt(mocker, default_conf, caplog) -> None:
patched_configuration_load_config_file(mocker, default_conf) patched_configuration_load_config_file(mocker, default_conf)
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
mocker.patch('freqtrade.wallets.Wallets.update', MagicMock()) mocker.patch('freqtrade.wallets.Wallets.update', MagicMock())
mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) mocker.patch('freqtrade.freqtradebot.init_db', MagicMock())
args = ['trade', '-c', 'config.json.example'] args = ['trade', '-c', 'config.json.example']
@ -104,7 +104,7 @@ def test_main_operational_exception(mocker, default_conf, caplog) -> None:
patched_configuration_load_config_file(mocker, default_conf) patched_configuration_load_config_file(mocker, default_conf)
mocker.patch('freqtrade.wallets.Wallets.update', MagicMock()) mocker.patch('freqtrade.wallets.Wallets.update', MagicMock())
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) mocker.patch('freqtrade.freqtradebot.init_db', MagicMock())
args = ['trade', '-c', 'config.json.example'] args = ['trade', '-c', 'config.json.example']
@ -155,7 +155,7 @@ def test_main_reload_config(mocker, default_conf, caplog) -> None:
reconfigure_mock = mocker.patch('freqtrade.worker.Worker._reconfigure', MagicMock()) reconfigure_mock = mocker.patch('freqtrade.worker.Worker._reconfigure', MagicMock())
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) mocker.patch('freqtrade.freqtradebot.init_db', MagicMock())
args = Arguments(['trade', '-c', 'config.json.example']).get_parsed_arg() args = Arguments(['trade', '-c', 'config.json.example']).get_parsed_arg()
worker = Worker(args=args, config=default_conf) worker = Worker(args=args, config=default_conf)
@ -178,7 +178,7 @@ def test_reconfigure(mocker, default_conf) -> None:
mocker.patch('freqtrade.wallets.Wallets.update', MagicMock()) mocker.patch('freqtrade.wallets.Wallets.update', MagicMock())
patched_configuration_load_config_file(mocker, default_conf) patched_configuration_load_config_file(mocker, default_conf)
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) mocker.patch('freqtrade.freqtradebot.init_db', MagicMock())
args = Arguments(['trade', '-c', 'config.json.example']).get_parsed_arg() args = Arguments(['trade', '-c', 'config.json.example']).get_parsed_arg()
worker = Worker(args=args, config=default_conf) worker = Worker(args=args, config=default_conf)

View File

@ -8,13 +8,13 @@ from sqlalchemy import create_engine
from freqtrade import constants from freqtrade import constants
from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.exceptions import DependencyException, OperationalException
from freqtrade.persistence import Order, Trade, clean_dry_run_db, init from freqtrade.persistence import Order, Trade, clean_dry_run_db, init_db
from tests.conftest import create_mock_trades, log_has, log_has_re from tests.conftest import create_mock_trades, log_has, log_has_re
def test_init_create_session(default_conf): def test_init_create_session(default_conf):
# Check if init create a session # Check if init create a session
init(default_conf['db_url'], default_conf['dry_run']) init_db(default_conf['db_url'], default_conf['dry_run'])
assert hasattr(Trade, 'session') assert hasattr(Trade, 'session')
assert 'scoped_session' in type(Trade.session).__name__ assert 'scoped_session' in type(Trade.session).__name__
@ -24,7 +24,7 @@ def test_init_custom_db_url(default_conf, mocker):
default_conf.update({'db_url': 'sqlite:///tmp/freqtrade2_test.sqlite'}) default_conf.update({'db_url': 'sqlite:///tmp/freqtrade2_test.sqlite'})
create_engine_mock = mocker.patch('freqtrade.persistence.models.create_engine', MagicMock()) create_engine_mock = mocker.patch('freqtrade.persistence.models.create_engine', MagicMock())
init(default_conf['db_url'], default_conf['dry_run']) init_db(default_conf['db_url'], default_conf['dry_run'])
assert create_engine_mock.call_count == 1 assert create_engine_mock.call_count == 1
assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tmp/freqtrade2_test.sqlite' assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tmp/freqtrade2_test.sqlite'
@ -33,7 +33,7 @@ def test_init_invalid_db_url(default_conf):
# Update path to a value other than default, but still in-memory # Update path to a value other than default, but still in-memory
default_conf.update({'db_url': 'unknown:///some.url'}) default_conf.update({'db_url': 'unknown:///some.url'})
with pytest.raises(OperationalException, match=r'.*no valid database URL*'): with pytest.raises(OperationalException, match=r'.*no valid database URL*'):
init(default_conf['db_url'], default_conf['dry_run']) init_db(default_conf['db_url'], default_conf['dry_run'])
def test_init_prod_db(default_conf, mocker): def test_init_prod_db(default_conf, mocker):
@ -42,7 +42,7 @@ def test_init_prod_db(default_conf, mocker):
create_engine_mock = mocker.patch('freqtrade.persistence.models.create_engine', MagicMock()) create_engine_mock = mocker.patch('freqtrade.persistence.models.create_engine', MagicMock())
init(default_conf['db_url'], default_conf['dry_run']) init_db(default_conf['db_url'], default_conf['dry_run'])
assert create_engine_mock.call_count == 1 assert create_engine_mock.call_count == 1
assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tradesv3.sqlite' assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tradesv3.sqlite'
@ -53,7 +53,7 @@ def test_init_dryrun_db(default_conf, mocker):
create_engine_mock = mocker.patch('freqtrade.persistence.models.create_engine', MagicMock()) create_engine_mock = mocker.patch('freqtrade.persistence.models.create_engine', MagicMock())
init(default_conf['db_url'], default_conf['dry_run']) init_db(default_conf['db_url'], default_conf['dry_run'])
assert create_engine_mock.call_count == 1 assert create_engine_mock.call_count == 1
assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tradesv3.dryrun.sqlite' assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tradesv3.dryrun.sqlite'
@ -482,7 +482,7 @@ def test_migrate_old(mocker, default_conf, fee):
engine.execute(insert_table_old) engine.execute(insert_table_old)
engine.execute(insert_table_old2) engine.execute(insert_table_old2)
# Run init to test migration # Run init to test migration
init(default_conf['db_url'], default_conf['dry_run']) init_db(default_conf['db_url'], default_conf['dry_run'])
assert len(Trade.query.filter(Trade.id == 1).all()) == 1 assert len(Trade.query.filter(Trade.id == 1).all()) == 1
trade = Trade.query.filter(Trade.id == 1).first() trade = Trade.query.filter(Trade.id == 1).first()
@ -581,7 +581,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
engine.execute("create table trades_bak1 as select * from trades") engine.execute("create table trades_bak1 as select * from trades")
# Run init to test migration # Run init to test migration
init(default_conf['db_url'], default_conf['dry_run']) init_db(default_conf['db_url'], default_conf['dry_run'])
assert len(Trade.query.filter(Trade.id == 1).all()) == 1 assert len(Trade.query.filter(Trade.id == 1).all()) == 1
trade = Trade.query.filter(Trade.id == 1).first() trade = Trade.query.filter(Trade.id == 1).first()
@ -661,7 +661,7 @@ def test_migrate_mid_state(mocker, default_conf, fee, caplog):
engine.execute(insert_table_old) engine.execute(insert_table_old)
# Run init to test migration # Run init to test migration
init(default_conf['db_url'], default_conf['dry_run']) init_db(default_conf['db_url'], default_conf['dry_run'])
assert len(Trade.query.filter(Trade.id == 1).all()) == 1 assert len(Trade.query.filter(Trade.id == 1).all()) == 1
trade = Trade.query.filter(Trade.id == 1).first() trade = Trade.query.filter(Trade.id == 1).first()
@ -904,7 +904,7 @@ def test_to_json(default_conf, fee):
def test_stoploss_reinitialization(default_conf, fee): def test_stoploss_reinitialization(default_conf, fee):
init(default_conf['db_url']) init_db(default_conf['db_url'])
trade = Trade( trade = Trade(
pair='ETH/BTC', pair='ETH/BTC',
stake_amount=0.001, stake_amount=0.001,