Merge branch 'develop' into list-pairs2

This commit is contained in:
hroff-1902
2019-10-20 23:22:45 +03:00
committed by GitHub
53 changed files with 1153 additions and 370 deletions

View File

@@ -39,7 +39,8 @@ ARGS_LIST_PAIRS = ["exchange", "print_list", "list_pairs_print_json", "print_one
ARGS_CREATE_USERDIR = ["user_data_dir"]
ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "exchange", "timeframes", "erase"]
ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "download_trades", "exchange",
"timeframes", "erase"]
ARGS_PLOT_DATAFRAME = ["pairs", "indicators1", "indicators2", "plot_limit", "db_url",
"trade_source", "export", "exportfilename", "timerange", "ticker_interval"]

View File

@@ -2,7 +2,6 @@
Definition of cli arguments used in arguments.py
"""
import argparse
import os
from freqtrade import __version__, constants
@@ -141,8 +140,6 @@ AVAILABLE_CLI_OPTIONS = {
'Requires `--export` to be set as well. '
'Example: `--export-filename=user_data/backtest_results/backtest_today.json`',
metavar='PATH',
default=os.path.join('user_data', 'backtest_results',
'backtest-result.json'),
),
"fee": Arg(
'--fee',
@@ -309,6 +306,12 @@ AVAILABLE_CLI_OPTIONS = {
type=check_int_positive,
metavar='INT',
),
"download_trades": Arg(
'--dl-trades',
help='Download trades instead of OHLCV data. The bot will resample trades to the '
'desired timeframe as specified as --timeframes/-t.',
action='store_true',
),
"exchange": Arg(
'--exchange',
help=f'Exchange name (default: `{constants.DEFAULT_EXCHANGE}`). '

View File

@@ -192,6 +192,13 @@ class Configuration:
config.update({'datadir': create_datadir(config, self.args.get("datadir", None))})
logger.info('Using data directory: %s ...', config.get('datadir'))
if self.args.get('exportfilename'):
self._args_to_config(config, argname='exportfilename',
logstring='Storing backtest results to {} ...')
else:
config['exportfilename'] = (config['user_data_dir']
/ 'backtest_results/backtest-result.json')
def _process_optimize_options(self, config: Dict[str, Any]) -> None:
# This will override the strategy configuration
@@ -235,9 +242,6 @@ class Configuration:
self._args_to_config(config, argname='export',
logstring='Parameter --export detected: {} ...')
self._args_to_config(config, argname='exportfilename',
logstring='Storing backtest results to {} ...')
# Edge section:
if 'stoploss_range' in self.args and self.args["stoploss_range"]:
txt_range = eval(self.args["stoploss_range"])
@@ -312,6 +316,8 @@ class Configuration:
self._args_to_config(config, argname='days',
logstring='Detected --days: {}')
self._args_to_config(config, argname='download_trades',
logstring='Detected --dl-trades: {}')
def _process_runmode(self, config: Dict[str, Any]) -> None:

View File

@@ -42,9 +42,10 @@ class TimeRange:
(r'^-(\d{10})$', (None, 'date')),
(r'^(\d{10})-$', ('date', None)),
(r'^(\d{10})-(\d{10})$', ('date', 'date')),
(r'^(-\d+)$', (None, 'line')),
(r'^(\d+)-$', ('line', None)),
(r'^(\d+)-(\d+)$', ('index', 'index'))]
(r'^-(\d{13})$', (None, 'date')),
(r'^(\d{13})-$', ('date', None)),
(r'^(\d{13})-(\d{13})$', ('date', 'date')),
]
for rex, stype in syntax:
# Apply the regular expression to text
match = re.match(rex, text)
@@ -57,6 +58,8 @@ class TimeRange:
starts = rvals[index]
if stype[0] == 'date' and len(starts) == 8:
start = arrow.get(starts, 'YYYYMMDD').timestamp
elif len(starts) == 13:
start = int(starts) // 1000
else:
start = int(starts)
index += 1
@@ -64,6 +67,8 @@ class TimeRange:
stops = rvals[index]
if stype[1] == 'date' and len(stops) == 8:
stop = arrow.get(stops, 'YYYYMMDD').timestamp
elif len(stops) == 13:
stop = int(stops) // 1000
else:
stop = int(stops)
return TimeRange(stype[0], stype[1], start, stop)

View File

@@ -10,7 +10,7 @@ DEFAULT_TICKER_INTERVAL = 5 # min
HYPEROPT_EPOCH = 100 # epochs
RETRY_TIMEOUT = 30 # sec
DEFAULT_STRATEGY = 'DefaultStrategy'
DEFAULT_HYPEROPT = 'DefaultHyperOpts'
DEFAULT_HYPEROPT = 'DefaultHyperOpt'
DEFAULT_HYPEROPT_LOSS = 'DefaultHyperOptLoss'
DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite'
DEFAULT_DB_DRYRUN_URL = 'sqlite://'
@@ -266,6 +266,6 @@ CONF_SCHEMA = {
'stake_amount',
'dry_run',
'bid_strategy',
'telegram'
'unfilledtimeout',
]
}

View File

@@ -93,7 +93,7 @@ def load_trades_from_db(db_url: str) -> pd.DataFrame:
t.close_date.replace(tzinfo=pytz.UTC) if t.close_date else None,
t.calc_profit(), t.calc_profit_percent(),
t.open_rate, t.close_rate, t.amount,
(t.close_date.timestamp() - t.open_date.timestamp()
(round((t.close_date.timestamp() - t.open_date.timestamp()) / 60, 2)
if t.close_date else None),
t.sell_reason,
t.fee_open, t.fee_close,

View File

@@ -114,3 +114,25 @@ def order_book_to_dataframe(bids: list, asks: list) -> DataFrame:
keys=['b_sum', 'b_size', 'bids', 'asks', 'a_size', 'a_sum'])
# logger.info('order book %s', frame )
return frame
def trades_to_ohlcv(trades: list, timeframe: str) -> list:
"""
Converts trades list to ohlcv list
:param trades: List of trades, as returned by ccxt.fetch_trades.
:param timeframe: Ticker timeframe to resample data to
:return: ohlcv timeframe as list (as returned by ccxt.fetch_ohlcv)
"""
from freqtrade.exchange import timeframe_to_minutes
ticker_minutes = timeframe_to_minutes(timeframe)
df = pd.DataFrame(trades)
df['datetime'] = pd.to_datetime(df['datetime'])
df = df.set_index('datetime')
df_new = df['price'].resample(f'{ticker_minutes}min').ohlc()
df_new['volume'] = df['amount'].resample(f'{ticker_minutes}min').sum()
df_new['date'] = df_new.index.astype("int64") // 10 ** 6
# Drop 0 volume rows
df_new = df_new.dropna()
columns = ["date", "open", "high", "low", "close", "volume"]
return list(zip(*[df_new[x].values.tolist() for x in columns]))

View File

@@ -17,7 +17,7 @@ from pandas import DataFrame
from freqtrade import OperationalException, misc
from freqtrade.configuration import TimeRange
from freqtrade.data.converter import parse_ticker_dataframe
from freqtrade.data.converter import parse_ticker_dataframe, trades_to_ohlcv
from freqtrade.exchange import Exchange, timeframe_to_minutes
logger = logging.getLogger(__name__)
@@ -33,20 +33,12 @@ def trim_tickerlist(tickerlist: List[Dict], timerange: TimeRange) -> List[Dict]:
start_index = 0
stop_index = len(tickerlist)
if timerange.starttype == 'line':
stop_index = timerange.startts
if timerange.starttype == 'index':
start_index = timerange.startts
elif timerange.starttype == 'date':
if timerange.starttype == 'date':
while (start_index < len(tickerlist) and
tickerlist[start_index][0] < timerange.startts * 1000):
start_index += 1
if timerange.stoptype == 'line':
start_index = max(len(tickerlist) + timerange.stopts, 0)
if timerange.stoptype == 'index':
stop_index = timerange.stopts
elif timerange.stoptype == 'date':
if timerange.stoptype == 'date':
while (stop_index > 0 and
tickerlist[stop_index-1][0] > timerange.stopts * 1000):
stop_index -= 1
@@ -82,6 +74,29 @@ def store_tickerdata_file(datadir: Path, pair: str,
misc.file_dump_json(filename, data, is_zip=is_zip)
def load_trades_file(datadir: Path, pair: str,
timerange: Optional[TimeRange] = None) -> List[Dict]:
"""
Load a pair from file, either .json.gz or .json
:return: tradelist or empty list if unsuccesful
"""
filename = pair_trades_filename(datadir, pair)
tradesdata = misc.file_load_json(filename)
if not tradesdata:
return []
return tradesdata
def store_trades_file(datadir: Path, pair: str,
data: list, is_zip: bool = True):
"""
Stores tickerdata to file
"""
filename = pair_trades_filename(datadir, pair)
misc.file_dump_json(filename, data, is_zip=is_zip)
def _validate_pairdata(pair, pairdata, timerange: TimeRange):
if timerange.starttype == 'date' and pairdata[0][0] > timerange.startts * 1000:
logger.warning('Missing data at start for pair %s, data starts at %s',
@@ -173,6 +188,12 @@ def pair_data_filename(datadir: Path, pair: str, ticker_interval: str) -> Path:
return filename
def pair_trades_filename(datadir: Path, pair: str) -> Path:
pair_s = pair.replace("/", "_")
filename = datadir.joinpath(f'{pair_s}-trades.json.gz')
return filename
def _load_cached_data_for_updating(datadir: Path, pair: str, ticker_interval: str,
timerange: Optional[TimeRange]) -> Tuple[List[Any],
Optional[int]]:
@@ -299,6 +320,92 @@ def refresh_backtest_ohlcv_data(exchange: Exchange, pairs: List[str], timeframes
return pairs_not_available
def download_trades_history(datadir: Path,
exchange: Exchange,
pair: str,
timerange: Optional[TimeRange] = None) -> bool:
"""
Download trade history from the exchange.
Appends to previously downloaded trades data.
"""
try:
since = timerange.startts * 1000 if timerange and timerange.starttype == 'date' else None
trades = load_trades_file(datadir, pair)
from_id = trades[-1]['id'] if trades else None
logger.debug("Current Start: %s", trades[0]['datetime'] if trades else 'None')
logger.debug("Current End: %s", trades[-1]['datetime'] if trades else 'None')
new_trades = exchange.get_historic_trades(pair=pair,
since=since if since else
int(arrow.utcnow().shift(
days=-30).float_timestamp) * 1000,
# until=xxx,
from_id=from_id,
)
trades.extend(new_trades[1])
store_trades_file(datadir, pair, trades)
logger.debug("New Start: %s", trades[0]['datetime'])
logger.debug("New End: %s", trades[-1]['datetime'])
logger.info(f"New Amount of trades: {len(trades)}")
return True
except Exception as e:
logger.error(
f'Failed to download historic trades for pair: "{pair}". '
f'Error: {e}'
)
return False
def refresh_backtest_trades_data(exchange: Exchange, pairs: List[str], datadir: Path,
timerange: TimeRange, erase=False) -> List[str]:
"""
Refresh stored trades data.
Used by freqtrade download-data
:return: Pairs not available
"""
pairs_not_available = []
for pair in pairs:
if pair not in exchange.markets:
pairs_not_available.append(pair)
logger.info(f"Skipping pair {pair}...")
continue
dl_file = pair_trades_filename(datadir, pair)
if erase and dl_file.exists():
logger.info(
f'Deleting existing data for pair {pair}.')
dl_file.unlink()
logger.info(f'Downloading trades for pair {pair}.')
download_trades_history(datadir=datadir, exchange=exchange,
pair=pair,
timerange=timerange)
return pairs_not_available
def convert_trades_to_ohlcv(pairs: List[str], timeframes: List[str],
datadir: Path, timerange: TimeRange, erase=False) -> None:
"""
Convert stored trades data to ohlcv data
"""
for pair in pairs:
trades = load_trades_file(datadir, pair)
for timeframe in timeframes:
ohlcv_file = pair_data_filename(datadir, pair, timeframe)
if erase and ohlcv_file.exists():
logger.info(f'Deleting existing data for pair {pair}, interval {timeframe}.')
ohlcv_file.unlink()
ohlcv = trades_to_ohlcv(trades, timeframe)
# Store ohlcv
store_tickerdata_file(datadir, pair, timeframe, data=ohlcv)
def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]:
"""
Get the maximum timeframe for the given backtest data

View File

@@ -1,4 +1,4 @@
from freqtrade.exchange.exchange import Exchange # noqa: F401
from freqtrade.exchange.exchange import Exchange, MAP_EXCHANGE_CHILDCLASS # noqa: F401
from freqtrade.exchange.exchange import (get_exchange_bad_reason, # noqa: F401
is_exchange_bad,
is_exchange_known_ccxt,

View File

@@ -16,6 +16,8 @@ class Binance(Exchange):
_ft_has: Dict = {
"stoploss_on_exchange": True,
"order_time_in_force": ['gtc', 'fok', 'ioc'],
"trades_pagination": "id",
"trades_pagination_arg": "fromId",
}
def get_order_book(self, pair: str, limit: int = 100) -> dict:

View File

@@ -103,6 +103,11 @@ BAD_EXCHANGES = {
], "Does not provide timeframes. ccxt fetchOHLCV: emulated"),
}
MAP_EXCHANGE_CHILDCLASS = {
'binanceus': 'binance',
'binanceje': 'binance',
}
def retrier_async(f):
async def wrapper(*args, **kwargs):
@@ -143,6 +148,8 @@ def retrier(f):
class Exchange:
_config: Dict = {}
# Parameters to add directly to buy/sell calls (like agreeing to trading agreement)
_params: Dict = {}
# Dict to specify which options each exchange implements
@@ -153,6 +160,9 @@ class Exchange:
"order_time_in_force": ["gtc"],
"ohlcv_candle_limit": 500,
"ohlcv_partial_candle": True,
"trades_pagination": "time", # Possible are "time" or "id"
"trades_pagination_arg": "since",
}
_ft_has: Dict = {}
@@ -196,6 +206,9 @@ class Exchange:
self._ohlcv_candle_limit = self._ft_has['ohlcv_candle_limit']
self._ohlcv_partial_candle = self._ft_has['ohlcv_partial_candle']
self._trades_pagination = self._ft_has['trades_pagination']
self._trades_pagination_arg = self._ft_has['trades_pagination_arg']
# Initialize ccxt objects
self._api = self._init_ccxt(
exchange_config, ccxt_kwargs=exchange_config.get('ccxt_config'))
@@ -760,6 +773,154 @@ class Exchange:
except ccxt.BaseError as e:
raise OperationalException(f'Could not fetch ticker data. Msg: {e}') from e
@retrier_async
async def _async_fetch_trades(self, pair: str,
since: Optional[int] = None,
params: Optional[dict] = None) -> List[Dict]:
"""
Asyncronously gets trade history using fetch_trades.
Handles exchange errors, does one call to the exchange.
:param pair: Pair to fetch trade data for
:param since: Since as integer timestamp in milliseconds
returns: List of dicts containing trades
"""
try:
# fetch trades asynchronously
if params:
logger.debug("Fetching trades for pair %s, params: %s ", pair, params)
trades = await self._api_async.fetch_trades(pair, params=params, limit=1000)
else:
logger.debug(
"Fetching trades for pair %s, since %s %s...",
pair, since,
'(' + arrow.get(since // 1000).isoformat() + ') ' if since is not None else ''
)
trades = await self._api_async.fetch_trades(pair, since=since, limit=1000)
return trades
except ccxt.NotSupported as e:
raise OperationalException(
f'Exchange {self._api.name} does not support fetching historical trade data.'
f'Message: {e}') from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(f'Could not load trade history due to {e.__class__.__name__}. '
f'Message: {e}') from e
except ccxt.BaseError as e:
raise OperationalException(f'Could not fetch trade data. Msg: {e}') from e
async def _async_get_trade_history_id(self, pair: str,
until: int,
since: Optional[int] = None,
from_id: Optional[str] = None) -> Tuple[str, List[Dict]]:
"""
Asyncronously gets trade history using fetch_trades
use this when exchange uses id-based iteration (check `self._trades_pagination`)
:param pair: Pair to fetch trade data for
:param since: Since as integer timestamp in milliseconds
:param until: Until as integer timestamp in milliseconds
:param from_id: Download data starting with ID (if id is known). Ignores "since" if set.
returns tuple: (pair, trades-list)
"""
trades: List[Dict] = []
if not from_id:
# Fetch first elements using timebased method to get an ID to paginate on
# Depending on the Exchange, this can introduce a drift at the start of the interval
# of up to an hour.
# e.g. Binance returns the "last 1000" candles within a 1h time interval
# - so we will miss the first trades.
t = await self._async_fetch_trades(pair, since=since)
from_id = t[-1]['id']
trades.extend(t[:-1])
while True:
t = await self._async_fetch_trades(pair,
params={self._trades_pagination_arg: from_id})
if len(t):
# Skip last id since its the key for the next call
trades.extend(t[:-1])
if from_id == t[-1]['id'] or t[-1]['timestamp'] > until:
logger.debug(f"Stopping because from_id did not change. "
f"Reached {t[-1]['timestamp']} > {until}")
# Reached the end of the defined-download period - add last trade as well.
trades.extend(t[-1:])
break
from_id = t[-1]['id']
else:
break
return (pair, trades)
async def _async_get_trade_history_time(self, pair: str, until: int,
since: Optional[int] = None) -> Tuple[str, List]:
"""
Asyncronously gets trade history using fetch_trades,
when the exchange uses time-based iteration (check `self._trades_pagination`)
:param pair: Pair to fetch trade data for
:param since: Since as integer timestamp in milliseconds
:param until: Until as integer timestamp in milliseconds
returns tuple: (pair, trades-list)
"""
trades: List[Dict] = []
while True:
t = await self._async_fetch_trades(pair, since=since)
if len(t):
since = t[-1]['timestamp']
trades.extend(t)
# Reached the end of the defined-download period
if until and t[-1]['timestamp'] > until:
logger.debug(
f"Stopping because until was reached. {t[-1]['timestamp']} > {until}")
break
else:
break
return (pair, trades)
async def _async_get_trade_history(self, pair: str,
since: Optional[int] = None,
until: Optional[int] = None,
from_id: Optional[str] = None) -> Tuple[str, List[Dict]]:
"""
Async wrapper handling downloading trades using either time or id based methods.
"""
if self._trades_pagination == 'time':
return await self._async_get_trade_history_time(
pair=pair, since=since,
until=until or ccxt.Exchange.milliseconds())
elif self._trades_pagination == 'id':
return await self._async_get_trade_history_id(
pair=pair, since=since,
until=until or ccxt.Exchange.milliseconds(), from_id=from_id
)
else:
raise OperationalException(f"Exchange {self.name} does use neither time, "
f"nor id based pagination")
def get_historic_trades(self, pair: str,
since: Optional[int] = None,
until: Optional[int] = None,
from_id: Optional[str] = None) -> Tuple[str, List]:
"""
Gets candle history using asyncio and returns the list of candles.
Handles all async doing.
Async over one pair, assuming we get `_ohlcv_candle_limit` candles per call.
:param pair: Pair to download
:param ticker_interval: Interval to get
:param since: Timestamp in milliseconds to get history from
:param until: Timestamp in milliseconds. Defaults to current timestamp if not defined.
:param from_id: Download data starting with ID (if id is known)
:returns List of tickers
"""
if not self.exchange_has("fetchTrades"):
raise OperationalException("This exchange does not suport downloading Trades.")
return asyncio.get_event_loop().run_until_complete(
self._async_get_trade_history(pair=pair, since=since,
until=until, from_id=from_id))
@retrier
def cancel_order(self, order_id: str, pair: str) -> None:
if self._config['dry_run']:

View File

@@ -14,6 +14,10 @@ logger = logging.getLogger(__name__)
class Kraken(Exchange):
_params: Dict = {"trading_agreement": "agree"}
_ft_has: Dict = {
"trades_pagination": "id",
"trades_pagination_arg": "since",
}
@retrier
def get_balances(self) -> dict:

View File

@@ -11,7 +11,7 @@ from typing import Any, Dict, List, Optional, Tuple
import arrow
from requests.exceptions import RequestException
from freqtrade import (DependencyException, OperationalException, InvalidOrderException,
from freqtrade import (DependencyException, InvalidOrderException,
__version__, constants, persistence)
from freqtrade.data.converter import order_book_to_dataframe
from freqtrade.data.dataprovider import DataProvider
@@ -466,12 +466,13 @@ class FreqtradeBot:
if result:
self.wallets.update()
def get_real_amount(self, trade: Trade, order: Dict) -> float:
def get_real_amount(self, trade: Trade, order: Dict, order_amount: float = None) -> float:
"""
Get real amount for the trade
Necessary for exchanges which charge fees in base currency (e.g. binance)
"""
order_amount = order['amount']
if order_amount is None:
order_amount = order['amount']
# Only run for closed orders
if trade.fee_open == 0 or order['status'] == 'open':
return order_amount
@@ -508,7 +509,7 @@ class FreqtradeBot:
if not isclose(amount, order_amount, abs_tol=constants.MATH_CLOSE_PREC):
logger.warning(f"Amount {amount} does not match amount {trade.amount}")
raise OperationalException("Half bought? Amounts don't match")
raise DependencyException("Half bought? Amounts don't match")
real_amount = amount - fee_abs
if fee_abs != 0:
logger.info(f"Applying fee on amount for {trade} "
@@ -536,7 +537,7 @@ class FreqtradeBot:
# Fee was applied, so set to 0
trade.fee_open = 0
except OperationalException as exception:
except DependencyException as exception:
logger.warning("Could not update trade amount: %s", exception)
trade.update(order)
@@ -705,7 +706,7 @@ class FreqtradeBot:
if trade.stop_loss > float(order['info']['stopPrice']):
# we check if the update is neccesary
update_beat = self.strategy.order_types.get('stoploss_on_exchange_interval', 60)
if (datetime.utcnow() - trade.stoploss_last_update).total_seconds() > update_beat:
if (datetime.utcnow() - trade.stoploss_last_update).total_seconds() >= update_beat:
# cancelling the current stoploss on exchange first
logger.info('Trailing stoploss: cancelling current stoploss on exchange (id:{%s})'
'in order to add another one ...', order['id'])
@@ -747,8 +748,8 @@ class FreqtradeBot:
"""
buy_timeout = self.config['unfilledtimeout']['buy']
sell_timeout = self.config['unfilledtimeout']['sell']
buy_timeoutthreashold = arrow.utcnow().shift(minutes=-buy_timeout).datetime
sell_timeoutthreashold = arrow.utcnow().shift(minutes=-sell_timeout).datetime
buy_timeout_threshold = arrow.utcnow().shift(minutes=-buy_timeout).datetime
sell_timeout_threshold = arrow.utcnow().shift(minutes=-sell_timeout).datetime
for trade in Trade.query.filter(Trade.open_order_id.isnot(None)).all():
try:
@@ -772,21 +773,18 @@ class FreqtradeBot:
self.wallets.update()
continue
# Handle cancelled on exchange
if order['status'] == 'canceled':
if order['side'] == 'buy':
self.handle_buy_order_full_cancel(trade, "canceled on Exchange")
elif order['side'] == 'sell':
self.handle_timedout_limit_sell(trade, order)
self.wallets.update()
# Check if order is still actually open
elif order['status'] == 'open':
if order['side'] == 'buy' and ordertime < buy_timeoutthreashold:
self.handle_timedout_limit_buy(trade, order)
self.wallets.update()
elif order['side'] == 'sell' and ordertime < sell_timeoutthreashold:
self.handle_timedout_limit_sell(trade, order)
self.wallets.update()
if ((order['side'] == 'buy' and order['status'] == 'canceled')
or (order['status'] == 'open'
and order['side'] == 'buy' and ordertime < buy_timeout_threshold)):
self.handle_timedout_limit_buy(trade, order)
self.wallets.update()
elif ((order['side'] == 'sell' and order['status'] == 'canceled')
or (order['status'] == 'open'
and order['side'] == 'sell' and ordertime < sell_timeout_threshold)):
self.handle_timedout_limit_sell(trade, order)
self.wallets.update()
def handle_buy_order_full_cancel(self, trade: Trade, reason: str) -> None:
"""Close trade in database and send message"""
@@ -802,16 +800,33 @@ class FreqtradeBot:
"""Buy timeout - cancel order
:return: True if order was fully cancelled
"""
self.exchange.cancel_order(trade.open_order_id, trade.pair)
if order['remaining'] == order['amount']:
reason = "cancelled due to timeout"
if order['status'] != 'canceled':
corder = self.exchange.cancel_order(trade.open_order_id, trade.pair)
else:
# Order was cancelled already, so we can reuse the existing dict
corder = order
reason = "canceled on Exchange"
if corder['remaining'] == corder['amount']:
# if trade is not partially completed, just delete the trade
self.handle_buy_order_full_cancel(trade, "cancelled due to timeout")
self.handle_buy_order_full_cancel(trade, reason)
return True
# if trade is partially complete, edit the stake details for the trade
# and close the order
trade.amount = order['amount'] - order['remaining']
trade.amount = corder['amount'] - corder['remaining']
trade.stake_amount = trade.amount * trade.open_rate
# verify if fees were taken from amount to avoid problems during selling
try:
new_amount = self.get_real_amount(trade, corder, trade.amount)
if not isclose(order['amount'], new_amount, abs_tol=constants.MATH_CLOSE_PREC):
trade.amount = new_amount
# Fee was applied, so set to 0
trade.fee_open = 0
except DependencyException as e:
logger.warning("Could not update trade amount: %s", e)
trade.open_order_id = None
logger.info('Partial buy order timeout for %s.', trade)
self.rpc.send_msg({

View File

@@ -72,8 +72,10 @@ def json_load(datafile: IO):
def file_load_json(file):
gzipfile = file.with_suffix(file.suffix + '.gz')
if file.suffix != ".gz":
gzipfile = file.with_suffix(file.suffix + '.gz')
else:
gzipfile = file
# Try gzip file first, otherwise regular json file.
if gzipfile.is_file():
logger.debug('Loading ticker data from file %s', gzipfile)

View File

@@ -11,7 +11,7 @@ import freqtrade.vendor.qtpylib.indicators as qtpylib
from freqtrade.optimize.hyperopt_interface import IHyperOpt
class DefaultHyperOpts(IHyperOpt):
class DefaultHyperOpt(IHyperOpt):
"""
Default hyperopt provided by the Freqtrade bot.
You can override it with your own Hyperopt

View File

@@ -3,7 +3,7 @@ This module loads custom exchanges
"""
import logging
from freqtrade.exchange import Exchange
from freqtrade.exchange import Exchange, MAP_EXCHANGE_CHILDCLASS
import freqtrade.exchange as exchanges
from freqtrade.resolvers import IResolver
@@ -22,6 +22,8 @@ class ExchangeResolver(IResolver):
Load the custom class from config parameter
:param config: configuration dictionary
"""
# Map exchange name to avoid duplicate classes for identical exchanges
exchange_name = MAP_EXCHANGE_CHILDCLASS.get(exchange_name, exchange_name)
exchange_name = exchange_name.title()
try:
self.exchange = self._load_exchange(exchange_name, kwargs={'config': config})

View File

@@ -52,14 +52,8 @@ class HyperOptResolver(IResolver):
"""
current_path = Path(__file__).parent.parent.joinpath('optimize').resolve()
abs_paths = [
config['user_data_dir'].joinpath('hyperopts'),
current_path,
]
if extra_dir:
# Add extra hyperopt directory on top of search paths
abs_paths.insert(0, Path(extra_dir).resolve())
abs_paths = self.build_search_paths(config, current_path=current_path,
user_subdir='hyperopts', extra_dir=extra_dir)
hyperopt = self._load_object(paths=abs_paths, object_type=IHyperOpt,
object_name=hyperopt_name, kwargs={'config': config})
@@ -109,14 +103,8 @@ class HyperOptLossResolver(IResolver):
"""
current_path = Path(__file__).parent.parent.joinpath('optimize').resolve()
abs_paths = [
config['user_data_dir'].joinpath('hyperopts'),
current_path,
]
if extra_dir:
# Add extra hyperopt directory on top of search paths
abs_paths.insert(0, Path(extra_dir).resolve())
abs_paths = self.build_search_paths(config, current_path=current_path,
user_subdir='hyperopts', extra_dir=extra_dir)
hyperoptloss = self._load_object(paths=abs_paths, object_type=IHyperOptLoss,
object_name=hyper_loss_name)

View File

@@ -7,7 +7,7 @@ import importlib.util
import inspect
import logging
from pathlib import Path
from typing import Any, List, Optional, Tuple, Type, Union
from typing import Any, List, Optional, Tuple, Union, Generator
logger = logging.getLogger(__name__)
@@ -17,15 +17,29 @@ class IResolver:
This class contains all the logic to load custom classes
"""
def build_search_paths(self, config, current_path: Path, user_subdir: str,
extra_dir: Optional[str] = None) -> List[Path]:
abs_paths = [
config['user_data_dir'].joinpath(user_subdir),
current_path,
]
if extra_dir:
# Add extra directory to the top of the search paths
abs_paths.insert(0, Path(extra_dir).resolve())
return abs_paths
@staticmethod
def _get_valid_object(object_type, module_path: Path,
object_name: str) -> Optional[Type[Any]]:
object_name: str) -> Generator[Any, None, None]:
"""
Returns the first object with matching object_type and object_name in the path given.
Generator returning objects with matching object_type and object_name in the path given.
:param object_type: object_type (class)
:param module_path: absolute path to the module
:param object_name: Class name of the object
:return: class or None
:return: generator containing matching objects
"""
# Generate spec based on absolute path
@@ -42,7 +56,7 @@ class IResolver:
obj for name, obj in inspect.getmembers(module, inspect.isclass)
if object_name == name and object_type in obj.__bases__
)
return next(valid_objects_gen, None)
return valid_objects_gen
@staticmethod
def _search_object(directory: Path, object_type, object_name: str,
@@ -59,9 +73,9 @@ class IResolver:
logger.debug('Ignoring %s', entry)
continue
module_path = entry.resolve()
obj = IResolver._get_valid_object(
object_type, module_path, object_name
)
obj = next(IResolver._get_valid_object(object_type, module_path, object_name), None)
if obj:
return (obj(**kwargs), module_path)
return (None, None)

View File

@@ -1,7 +1,7 @@
# pragma pylint: disable=attribute-defined-outside-init
"""
This module load custom hyperopts
This module load custom pairlists
"""
import logging
from pathlib import Path
@@ -15,7 +15,7 @@ logger = logging.getLogger(__name__)
class PairListResolver(IResolver):
"""
This class contains all the logic to load custom hyperopt class
This class contains all the logic to load custom PairList class
"""
__slots__ = ['pairlist']
@@ -39,10 +39,8 @@ class PairListResolver(IResolver):
"""
current_path = Path(__file__).parent.parent.joinpath('pairlist').resolve()
abs_paths = [
config['user_data_dir'].joinpath('pairlist'),
current_path,
]
abs_paths = self.build_search_paths(config, current_path=current_path,
user_subdir='pairlist', extra_dir=None)
pairlist = self._load_object(paths=abs_paths, object_type=IPairList,
object_name=pairlist_name, kwargs=kwargs)

View File

@@ -95,7 +95,10 @@ class StrategyResolver(IResolver):
logger.info("Override strategy '%s' with value in config file: %s.",
attribute, config[attribute])
elif hasattr(self.strategy, attribute):
config[attribute] = getattr(self.strategy, attribute)
val = getattr(self.strategy, attribute)
# None's cannot exist in the config, so do not copy them
if val is not None:
config[attribute] = val
# Explicitly check for None here as other "falsy" values are possible
elif default is not None:
setattr(self.strategy, attribute, default)
@@ -121,14 +124,8 @@ class StrategyResolver(IResolver):
"""
current_path = Path(__file__).parent.parent.joinpath('strategy').resolve()
abs_paths = [
config['user_data_dir'].joinpath('strategies'),
current_path,
]
if extra_dir:
# Add extra strategy directory on top of search paths
abs_paths.insert(0, Path(extra_dir).resolve())
abs_paths = self.build_search_paths(config, current_path=current_path,
user_subdir='strategies', extra_dir=extra_dir)
if ":" in strategy_name:
logger.info("loading base64 encoded strategy")

View File

@@ -18,7 +18,7 @@ class RPCManager:
self.registered_modules: List[RPC] = []
# Enable telegram
if freqtrade.config['telegram'].get('enabled', False):
if freqtrade.config.get('telegram', {}).get('enabled', False):
logger.info('Enabling rpc.telegram ...')
from freqtrade.rpc.telegram import Telegram
self.registered_modules.append(Telegram(freqtrade))

View File

@@ -78,8 +78,8 @@ class IStrategy(ABC):
# trailing stoploss
trailing_stop: bool = False
trailing_stop_positive: float
trailing_stop_positive_offset: float
trailing_stop_positive: Optional[float] = None
trailing_stop_positive_offset: float = 0.0
trailing_only_offset_is_reached = False
# associated ticker interval
@@ -347,26 +347,23 @@ class IStrategy(ABC):
decides to sell or not
:param current_profit: current profit in percent
"""
trailing_stop = self.config.get('trailing_stop', False)
stop_loss_value = force_stoploss if force_stoploss else self.stoploss
# Initiate stoploss with open_rate. Does nothing if stoploss is already set.
trade.adjust_stop_loss(trade.open_rate, stop_loss_value, initial=True)
if trailing_stop:
if self.trailing_stop:
# trailing stoploss handling
sl_offset = self.config.get('trailing_stop_positive_offset') or 0.0
tsl_only_offset = self.config.get('trailing_only_offset_is_reached', False)
sl_offset = self.trailing_stop_positive_offset
# Make sure current_profit is calculated using high for backtesting.
high_profit = current_profit if not high else trade.calc_profit_percent(high)
# Don't update stoploss if trailing_only_offset_is_reached is true.
if not (tsl_only_offset and high_profit < sl_offset):
if not (self.trailing_only_offset_is_reached and high_profit < sl_offset):
# Specific handling for trailing_stop_positive
if 'trailing_stop_positive' in self.config and high_profit > sl_offset:
# Ignore mypy error check in configuration that this is a float
stop_loss_value = self.config.get('trailing_stop_positive') # type: ignore
if self.trailing_stop_positive is not None and high_profit > sl_offset:
stop_loss_value = self.trailing_stop_positive
logger.debug(f"{trade.pair} - Using positive stoploss: {stop_loss_value} "
f"offset: {sl_offset:.4g} profit: {current_profit:.4f}%")

View File

@@ -12,7 +12,9 @@ from tabulate import tabulate
from freqtrade import OperationalException
from freqtrade.configuration import Configuration, TimeRange
from freqtrade.configuration.directory_operations import create_userdata_dir
from freqtrade.data.history import refresh_backtest_ohlcv_data
from freqtrade.data.history import (convert_trades_to_ohlcv,
refresh_backtest_ohlcv_data,
refresh_backtest_trades_data)
from freqtrade.exchange import (available_exchanges, ccxt_exchanges, market_is_active,
symbol_is_pair)
from freqtrade.misc import plural
@@ -94,9 +96,19 @@ def start_download_data(args: Dict[str, Any]) -> None:
# Init exchange
exchange = ExchangeResolver(config['exchange']['name'], config).exchange
pairs_not_available = refresh_backtest_ohlcv_data(
exchange, pairs=config["pairs"], timeframes=config["timeframes"],
dl_path=Path(config['datadir']), timerange=timerange, erase=config.get("erase"))
if config.get('download_trades'):
pairs_not_available = refresh_backtest_trades_data(
exchange, pairs=config["pairs"], datadir=Path(config['datadir']),
timerange=timerange, erase=config.get("erase"))
# Convert downloaded trade data to different timeframes
convert_trades_to_ohlcv(
pairs=config["pairs"], timeframes=config["timeframes"],
datadir=Path(config['datadir']), timerange=timerange, erase=config.get("erase"))
else:
pairs_not_available = refresh_backtest_ohlcv_data(
exchange, pairs=config["pairs"], timeframes=config["timeframes"],
dl_path=Path(config['datadir']), timerange=timerange, erase=config.get("erase"))
except KeyboardInterrupt:
sys.exit("SIGINT received, aborting ...")