Merge branch 'develop' into spreadfilter
This commit is contained in:
commit
d2cac1d8fd
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@ -18,7 +18,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ ubuntu-18.04, macos-latest ]
|
||||
python-version: [3.7]
|
||||
python-version: [3.7, 3.8]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
@ -68,7 +68,7 @@ jobs:
|
||||
pytest --random-order --cov=freqtrade --cov-config=.coveragerc
|
||||
|
||||
- name: Coveralls
|
||||
if: startsWith(matrix.os, 'ubuntu')
|
||||
if: (startsWith(matrix.os, 'ubuntu') && matrix.python-version == '3.8')
|
||||
env:
|
||||
# Coveralls token. Not used as secret due to github not providing secrets to forked repositories
|
||||
COVERALLS_REPO_TOKEN: 6D1m0xupS3FgutfuGao8keFf9Hc0FpIXu
|
||||
|
@ -1,4 +1,4 @@
|
||||
FROM python:3.7.6-slim-stretch
|
||||
FROM python:3.8.1-slim-buster
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get -y install curl build-essential libssl-dev \
|
||||
|
@ -278,7 +278,7 @@ If this is configured, the following 4 values (`buy`, `sell`, `stoploss` and
|
||||
The below is the default which is used if this is not configured in either strategy or configuration file.
|
||||
|
||||
Since `stoploss_on_exchange` uses limit orders, the exchange needs 2 prices, the stoploss_price and the Limit price.
|
||||
`stoploss` defines the stop-price - and limit should be slightly below this. This defaults to 0.99 / 1%.
|
||||
`stoploss` defines the stop-price - and limit should be slightly below this. This defaults to 0.99 / 1% (configurable via `stoploss_on_exchange_limit_ratio`).
|
||||
Calculation example: we bought the asset at 100$.
|
||||
Stop-price is 95$, then limit would be `95 * 0.99 = 94.05$` - so the stoploss will happen between 95$ and 94.05$.
|
||||
|
||||
|
@ -5,7 +5,7 @@ This page combines common gotchas and informations which are exchange-specific a
|
||||
## Binance
|
||||
|
||||
!!! Tip "Stoploss on Exchange"
|
||||
Binance is currently the only exchange supporting `stoploss_on_exchange`. It provides great advantages, so we recommend to benefit from it.
|
||||
Binance supports `stoploss_on_exchange` and uses stop-loss-limit orders. It provides great advantages, so we recommend to benefit from it.
|
||||
|
||||
### Blacklists
|
||||
|
||||
@ -22,6 +22,9 @@ Binance has been split into 3, and users must use the correct ccxt exchange ID f
|
||||
|
||||
## Kraken
|
||||
|
||||
!!! Tip "Stoploss on Exchange"
|
||||
Kraken supports `stoploss_on_exchange` and uses stop-loss-market orders. It provides great advantages, so we recommend to benefit from it, however since the resulting order is a stoploss-market order, sell-rates are not guaranteed, which makes this feature less secure than on other exchanges. This limitation is based on kraken's policy [source](https://blog.kraken.com/post/1234/announcement-delisting-pairs-and-temporary-suspension-of-advanced-order-types/) and [source2](https://blog.kraken.com/post/1494/kraken-enables-advanced-orders-and-adds-10-currency-pairs/) - which has stoploss-limit orders disabled.
|
||||
|
||||
### Historic Kraken data
|
||||
|
||||
The Kraken API does only provide 720 historic candles, which is sufficient for Freqtrade dry-run and live trade modes, but is a problem for backtesting.
|
||||
|
@ -27,7 +27,7 @@ So this parameter will tell the bot how often it should update the stoploss orde
|
||||
This same logic will reapply a stoploss order on the exchange should you cancel it accidentally.
|
||||
|
||||
!!! Note
|
||||
Stoploss on exchange is only supported for Binance as of now.
|
||||
Stoploss on exchange is only supported for Binance (stop-loss-limit) and Kraken (stop-loss-market) as of now.
|
||||
|
||||
## Static Stop Loss
|
||||
|
||||
|
@ -43,16 +43,18 @@ def start_download_data(args: Dict[str, Any]) -> None:
|
||||
if config.get('download_trades'):
|
||||
pairs_not_available = refresh_backtest_trades_data(
|
||||
exchange, pairs=config["pairs"], datadir=config['datadir'],
|
||||
timerange=timerange, erase=config.get("erase"))
|
||||
timerange=timerange, erase=bool(config.get("erase")))
|
||||
|
||||
# Convert downloaded trade data to different timeframes
|
||||
convert_trades_to_ohlcv(
|
||||
pairs=config["pairs"], timeframes=config["timeframes"],
|
||||
datadir=config['datadir'], timerange=timerange, erase=config.get("erase"))
|
||||
datadir=config['datadir'], timerange=timerange,
|
||||
erase=bool(config.get("erase")))
|
||||
else:
|
||||
pairs_not_available = refresh_backtest_ohlcv_data(
|
||||
exchange, pairs=config["pairs"], timeframes=config["timeframes"],
|
||||
datadir=config['datadir'], timerange=timerange, erase=config.get("erase"))
|
||||
datadir=config['datadir'], timerange=timerange,
|
||||
erase=bool(config.get("erase")))
|
||||
|
||||
except KeyboardInterrupt:
|
||||
sys.exit("SIGINT received, aborting ...")
|
||||
|
@ -28,7 +28,7 @@ def start_create_userdir(args: Dict[str, Any]) -> None:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def deploy_new_strategy(strategy_name, strategy_path: Path, subtemplate: str):
|
||||
def deploy_new_strategy(strategy_name: str, strategy_path: Path, subtemplate: str) -> None:
|
||||
"""
|
||||
Deploy new strategy from template to strategy_path
|
||||
"""
|
||||
@ -69,7 +69,7 @@ def start_new_strategy(args: Dict[str, Any]) -> None:
|
||||
raise OperationalException("`new-strategy` requires --strategy to be set.")
|
||||
|
||||
|
||||
def deploy_new_hyperopt(hyperopt_name, hyperopt_path: Path, subtemplate: str):
|
||||
def deploy_new_hyperopt(hyperopt_name: str, hyperopt_path: Path, subtemplate: str) -> None:
|
||||
"""
|
||||
Deploys a new hyperopt template to hyperopt_path
|
||||
"""
|
||||
|
@ -5,7 +5,7 @@ from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.state import RunMode
|
||||
|
||||
|
||||
def validate_plot_args(args: Dict[str, Any]):
|
||||
def validate_plot_args(args: Dict[str, Any]) -> None:
|
||||
if not args.get('datadir') and not args.get('config'):
|
||||
raise OperationalException(
|
||||
"You need to specify either `--datadir` or `--config` "
|
||||
|
@ -10,7 +10,7 @@ from freqtrade.state import RunMode
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def remove_credentials(config: Dict[str, Any]):
|
||||
def remove_credentials(config: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Removes exchange keys from the configuration and specifies dry-run
|
||||
Used for backtesting / hyperopt / edge and utils.
|
||||
|
@ -13,7 +13,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
def check_conflicting_settings(config: Dict[str, Any],
|
||||
section1: str, name1: str,
|
||||
section2: str, name2: str):
|
||||
section2: str, name2: str) -> None:
|
||||
section1_config = config.get(section1, {})
|
||||
section2_config = config.get(section2, {})
|
||||
if name1 in section1_config and name2 in section2_config:
|
||||
@ -28,7 +28,7 @@ def check_conflicting_settings(config: Dict[str, Any],
|
||||
|
||||
def process_deprecated_setting(config: Dict[str, Any],
|
||||
section1: str, name1: str,
|
||||
section2: str, name2: str):
|
||||
section2: str, name2: str) -> None:
|
||||
section2_config = config.get(section2, {})
|
||||
|
||||
if name2 in section2_config:
|
||||
|
@ -23,7 +23,7 @@ def create_datadir(config: Dict[str, Any], datadir: Optional[str] = None) -> Pat
|
||||
return folder
|
||||
|
||||
|
||||
def create_userdata_dir(directory: str, create_dir=False) -> Path:
|
||||
def create_userdata_dir(directory: str, create_dir: bool = False) -> Path:
|
||||
"""
|
||||
Create userdata directory structure.
|
||||
if create_dir is True, then the parent-directory will be created if it does not exist.
|
||||
|
@ -7,6 +7,7 @@ from typing import Optional
|
||||
|
||||
import arrow
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ -30,7 +31,7 @@ class TimeRange:
|
||||
return (self.starttype == other.starttype and self.stoptype == other.stoptype
|
||||
and self.startts == other.startts and self.stopts == other.stopts)
|
||||
|
||||
def subtract_start(self, seconds) -> None:
|
||||
def subtract_start(self, seconds: int) -> None:
|
||||
"""
|
||||
Subtracts <seconds> from startts if startts is set.
|
||||
:param seconds: Seconds to subtract from starttime
|
||||
@ -59,7 +60,7 @@ class TimeRange:
|
||||
self.starttype = 'date'
|
||||
|
||||
@staticmethod
|
||||
def parse_timerange(text: Optional[str]):
|
||||
def parse_timerange(text: Optional[str]) -> 'TimeRange':
|
||||
"""
|
||||
Parse the value of the argument --timerange to determine what is the range desired
|
||||
:param text: value from --timerange
|
||||
|
@ -3,7 +3,7 @@ Helpers when analyzing backtest data
|
||||
"""
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Dict
|
||||
from typing import Dict, Union
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
@ -20,7 +20,7 @@ BT_DATA_COLUMNS = ["pair", "profitperc", "open_time", "close_time", "index", "du
|
||||
"open_rate", "close_rate", "open_at_end", "sell_reason"]
|
||||
|
||||
|
||||
def load_backtest_data(filename) -> pd.DataFrame:
|
||||
def load_backtest_data(filename: Union[Path, str]) -> pd.DataFrame:
|
||||
"""
|
||||
Load backtest data file.
|
||||
:param filename: pathlib.Path object, or string pointing to the file.
|
||||
@ -151,7 +151,8 @@ def extract_trades_of_period(dataframe: pd.DataFrame, trades: pd.DataFrame) -> p
|
||||
return trades
|
||||
|
||||
|
||||
def combine_tickers_with_mean(tickers: Dict[str, pd.DataFrame], column: str = "close"):
|
||||
def combine_tickers_with_mean(tickers: Dict[str, pd.DataFrame],
|
||||
column: str = "close") -> pd.DataFrame:
|
||||
"""
|
||||
Combine multiple dataframes "column"
|
||||
:param tickers: Dict of Dataframes, dict key should be pair.
|
||||
|
@ -86,7 +86,7 @@ def load_tickerdata_file(datadir: Path, pair: str, timeframe: str,
|
||||
|
||||
|
||||
def store_tickerdata_file(datadir: Path, pair: str,
|
||||
timeframe: str, data: list, is_zip: bool = False):
|
||||
timeframe: str, data: list, is_zip: bool = False) -> None:
|
||||
"""
|
||||
Stores tickerdata to file
|
||||
"""
|
||||
@ -109,7 +109,7 @@ def load_trades_file(datadir: Path, pair: str,
|
||||
|
||||
|
||||
def store_trades_file(datadir: Path, pair: str,
|
||||
data: list, is_zip: bool = True):
|
||||
data: list, is_zip: bool = True) -> None:
|
||||
"""
|
||||
Stores tickerdata to file
|
||||
"""
|
||||
@ -117,7 +117,7 @@ def store_trades_file(datadir: Path, pair: str,
|
||||
misc.file_dump_json(filename, data, is_zip=is_zip)
|
||||
|
||||
|
||||
def _validate_pairdata(pair, pairdata, timerange: TimeRange):
|
||||
def _validate_pairdata(pair: str, pairdata: List[Dict], timerange: TimeRange) -> None:
|
||||
if timerange.starttype == 'date' and pairdata[0][0] > timerange.startts * 1000:
|
||||
logger.warning('Missing data at start for pair %s, data starts at %s',
|
||||
pair, arrow.get(pairdata[0][0] // 1000).strftime('%Y-%m-%d %H:%M:%S'))
|
||||
@ -331,7 +331,7 @@ def _download_pair_history(datadir: Path,
|
||||
|
||||
def refresh_backtest_ohlcv_data(exchange: Exchange, pairs: List[str], timeframes: List[str],
|
||||
datadir: Path, timerange: Optional[TimeRange] = None,
|
||||
erase=False) -> List[str]:
|
||||
erase: bool = False) -> List[str]:
|
||||
"""
|
||||
Refresh stored ohlcv data for backtesting and hyperopt operations.
|
||||
Used by freqtrade download-data subcommand.
|
||||
@ -401,7 +401,7 @@ def _download_trades_history(datadir: Path,
|
||||
|
||||
|
||||
def refresh_backtest_trades_data(exchange: Exchange, pairs: List[str], datadir: Path,
|
||||
timerange: TimeRange, erase=False) -> List[str]:
|
||||
timerange: TimeRange, erase: bool = False) -> List[str]:
|
||||
"""
|
||||
Refresh stored trades data for backtesting and hyperopt operations.
|
||||
Used by freqtrade download-data subcommand.
|
||||
@ -428,7 +428,7 @@ def refresh_backtest_trades_data(exchange: Exchange, pairs: List[str], datadir:
|
||||
|
||||
|
||||
def convert_trades_to_ohlcv(pairs: List[str], timeframes: List[str],
|
||||
datadir: Path, timerange: TimeRange, erase=False) -> None:
|
||||
datadir: Path, timerange: TimeRange, erase: bool = False) -> None:
|
||||
"""
|
||||
Convert stored trades data to ohlcv data
|
||||
"""
|
||||
|
@ -1,7 +1,7 @@
|
||||
# pragma pylint: disable=W0603
|
||||
""" Edge positioning package """
|
||||
import logging
|
||||
from typing import Any, Dict, NamedTuple
|
||||
from typing import Any, Dict, List, NamedTuple
|
||||
|
||||
import arrow
|
||||
import numpy as np
|
||||
@ -181,7 +181,7 @@ class Edge:
|
||||
'strategy stoploss is returned instead.')
|
||||
return self.strategy.stoploss
|
||||
|
||||
def adjust(self, pairs) -> list:
|
||||
def adjust(self, pairs: List[str]) -> list:
|
||||
"""
|
||||
Filters out and sorts "pairs" according to Edge calculated pairs
|
||||
"""
|
||||
|
@ -32,13 +32,23 @@ class Binance(Exchange):
|
||||
|
||||
return super().get_order_book(pair, limit)
|
||||
|
||||
def stoploss_limit(self, pair: str, amount: float, stop_price: float, rate: float) -> Dict:
|
||||
def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool:
|
||||
"""
|
||||
Verify stop_loss against stoploss-order value (limit or price)
|
||||
Returns True if adjustment is necessary.
|
||||
"""
|
||||
return order['type'] == 'stop_loss_limit' and stop_loss > float(order['info']['stopPrice'])
|
||||
|
||||
def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict:
|
||||
"""
|
||||
creates a stoploss limit order.
|
||||
this stoploss-limit is binance-specific.
|
||||
It may work with a limited number of other exchanges, but this has not been tested yet.
|
||||
|
||||
"""
|
||||
# Limit price threshold: As limit price should always be below stop-price
|
||||
limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99)
|
||||
rate = stop_price * limit_price_pct
|
||||
|
||||
ordertype = "stop_loss_limit"
|
||||
|
||||
stop_price = self.price_to_precision(pair, stop_price)
|
||||
@ -61,8 +71,8 @@ class Binance(Exchange):
|
||||
|
||||
rate = self.price_to_precision(pair, rate)
|
||||
|
||||
order = self._api.create_order(pair, ordertype, 'sell',
|
||||
amount, rate, params)
|
||||
order = self._api.create_order(symbol=pair, type=ordertype, side='sell',
|
||||
amount=amount, price=stop_price, params=params)
|
||||
logger.info('stoploss limit order added for %s. '
|
||||
'stop price: %s. limit: %s', pair, stop_price, rate)
|
||||
return order
|
||||
|
@ -24,6 +24,10 @@ from freqtrade.exceptions import (DependencyException, InvalidOrderException,
|
||||
from freqtrade.exchange.common import BAD_EXCHANGES, retrier, retrier_async
|
||||
from freqtrade.misc import deep_merge_dicts
|
||||
|
||||
|
||||
CcxtModuleType = Any
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ -51,7 +55,7 @@ class Exchange:
|
||||
}
|
||||
_ft_has: Dict = {}
|
||||
|
||||
def __init__(self, config: dict, validate: bool = True) -> None:
|
||||
def __init__(self, config: Dict[str, Any], validate: bool = True) -> None:
|
||||
"""
|
||||
Initializes this module with the given config,
|
||||
it does basic validation whether the specified exchange and pairs are valid.
|
||||
@ -135,7 +139,7 @@ class Exchange:
|
||||
if self._api_async and inspect.iscoroutinefunction(self._api_async.close):
|
||||
asyncio.get_event_loop().run_until_complete(self._api_async.close())
|
||||
|
||||
def _init_ccxt(self, exchange_config: dict, ccxt_module=ccxt,
|
||||
def _init_ccxt(self, exchange_config: Dict[str, Any], ccxt_module: CcxtModuleType = ccxt,
|
||||
ccxt_kwargs: dict = None) -> ccxt.Exchange:
|
||||
"""
|
||||
Initialize ccxt with given config and return valid
|
||||
@ -224,13 +228,13 @@ class Exchange:
|
||||
markets = self.markets
|
||||
return sorted(set([x['quote'] for _, x in markets.items()]))
|
||||
|
||||
def klines(self, pair_interval: Tuple[str, str], copy=True) -> DataFrame:
|
||||
def klines(self, pair_interval: Tuple[str, str], copy: bool = True) -> DataFrame:
|
||||
if pair_interval in self._klines:
|
||||
return self._klines[pair_interval].copy() if copy else self._klines[pair_interval]
|
||||
else:
|
||||
return DataFrame()
|
||||
|
||||
def set_sandbox(self, api, exchange_config: dict, name: str):
|
||||
def set_sandbox(self, api: ccxt.Exchange, exchange_config: dict, name: str) -> None:
|
||||
if exchange_config.get('sandbox'):
|
||||
if api.urls.get('test'):
|
||||
api.urls['api'] = api.urls['test']
|
||||
@ -240,7 +244,7 @@ class Exchange:
|
||||
"Please check your config.json")
|
||||
raise OperationalException(f'Exchange {name} does not provide a sandbox api')
|
||||
|
||||
def _load_async_markets(self, reload=False) -> None:
|
||||
def _load_async_markets(self, reload: bool = False) -> None:
|
||||
try:
|
||||
if self._api_async:
|
||||
asyncio.get_event_loop().run_until_complete(
|
||||
@ -273,7 +277,7 @@ class Exchange:
|
||||
except ccxt.BaseError:
|
||||
logger.exception("Could not reload markets.")
|
||||
|
||||
def validate_stakecurrency(self, stake_currency) -> None:
|
||||
def validate_stakecurrency(self, stake_currency: str) -> None:
|
||||
"""
|
||||
Checks stake-currency against available currencies on the exchange.
|
||||
:param stake_currency: Stake-currency to validate
|
||||
@ -282,8 +286,8 @@ class Exchange:
|
||||
quote_currencies = self.get_quote_currencies()
|
||||
if stake_currency not in quote_currencies:
|
||||
raise OperationalException(
|
||||
f"{stake_currency} is not available as stake on {self.name}. "
|
||||
f"Available currencies are: {', '.join(quote_currencies)}")
|
||||
f"{stake_currency} is not available as stake on {self.name}. "
|
||||
f"Available currencies are: {', '.join(quote_currencies)}")
|
||||
|
||||
def validate_pairs(self, pairs: List[str]) -> None:
|
||||
"""
|
||||
@ -319,7 +323,7 @@ class Exchange:
|
||||
f"Please check if you are impacted by this restriction "
|
||||
f"on the exchange and eventually remove {pair} from your whitelist.")
|
||||
|
||||
def get_valid_pair_combination(self, curr_1, curr_2) -> str:
|
||||
def get_valid_pair_combination(self, curr_1: str, curr_2: str) -> str:
|
||||
"""
|
||||
Get valid pair combination of curr_1 and curr_2 by trying both combinations.
|
||||
"""
|
||||
@ -373,7 +377,7 @@ class Exchange:
|
||||
raise OperationalException(
|
||||
f'Time in force policies are not supported for {self.name} yet.')
|
||||
|
||||
def validate_required_startup_candles(self, startup_candles) -> None:
|
||||
def validate_required_startup_candles(self, startup_candles: int) -> None:
|
||||
"""
|
||||
Checks if required startup_candles is more than ohlcv_candle_limit.
|
||||
Requires a grace-period of 5 candles - so a startup-period up to 494 is allowed by default.
|
||||
@ -392,7 +396,7 @@ class Exchange:
|
||||
"""
|
||||
return endpoint in self._api.has and self._api.has[endpoint]
|
||||
|
||||
def amount_to_precision(self, pair, amount: float) -> float:
|
||||
def amount_to_precision(self, pair: str, amount: float) -> float:
|
||||
'''
|
||||
Returns the amount to buy or sell to a precision the Exchange accepts
|
||||
Reimplementation of ccxt internal methods - ensuring we can test the result is correct
|
||||
@ -406,7 +410,7 @@ class Exchange:
|
||||
|
||||
return amount
|
||||
|
||||
def price_to_precision(self, pair, price: float) -> float:
|
||||
def price_to_precision(self, pair: str, price: float) -> float:
|
||||
'''
|
||||
Returns the price rounded up to the precision the Exchange accepts.
|
||||
Partial Reimplementation of ccxt internal method decimal_to_precision(),
|
||||
@ -460,7 +464,7 @@ class Exchange:
|
||||
"status": "closed",
|
||||
"filled": closed_order["amount"],
|
||||
"remaining": 0
|
||||
})
|
||||
})
|
||||
if closed_order["type"] in ["stop_loss_limit"]:
|
||||
closed_order["info"].update({"stopPrice": closed_order["price"]})
|
||||
self._dry_run_open_orders[closed_order["id"]] = closed_order
|
||||
@ -494,7 +498,7 @@ class Exchange:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
def buy(self, pair: str, ordertype: str, amount: float,
|
||||
rate: float, time_in_force) -> Dict:
|
||||
rate: float, time_in_force: str) -> Dict:
|
||||
|
||||
if self._config['dry_run']:
|
||||
dry_order = self.dry_run_order(pair, ordertype, "buy", amount, rate)
|
||||
@ -507,7 +511,7 @@ class Exchange:
|
||||
return self.create_order(pair, ordertype, 'buy', amount, rate, params)
|
||||
|
||||
def sell(self, pair: str, ordertype: str, amount: float,
|
||||
rate: float, time_in_force='gtc') -> Dict:
|
||||
rate: float, time_in_force: str = 'gtc') -> Dict:
|
||||
|
||||
if self._config['dry_run']:
|
||||
dry_order = self.dry_run_order(pair, ordertype, "sell", amount, rate)
|
||||
@ -519,9 +523,17 @@ class Exchange:
|
||||
|
||||
return self.create_order(pair, ordertype, 'sell', amount, rate, params)
|
||||
|
||||
def stoploss_limit(self, pair: str, amount: float, stop_price: float, rate: float) -> Dict:
|
||||
def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool:
|
||||
"""
|
||||
creates a stoploss limit order.
|
||||
Verify stop_loss against stoploss-order value (limit or price)
|
||||
Returns True if adjustment is necessary.
|
||||
"""
|
||||
raise OperationalException(f"stoploss is not implemented for {self.name}.")
|
||||
|
||||
def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict:
|
||||
"""
|
||||
creates a stoploss order.
|
||||
The precise ordertype is determined by the order_types dict or exchange default.
|
||||
Since ccxt does not unify stoploss-limit orders yet, this needs to be implemented in each
|
||||
exchange's subclass.
|
||||
The exception below should never raise, since we disallow
|
||||
@ -529,7 +541,7 @@ class Exchange:
|
||||
Note: Changes to this interface need to be applied to all sub-classes too.
|
||||
"""
|
||||
|
||||
raise OperationalException(f"stoploss_limit is not implemented for {self.name}.")
|
||||
raise OperationalException(f"stoploss is not implemented for {self.name}.")
|
||||
|
||||
@retrier
|
||||
def get_balance(self, currency: str) -> float:
|
||||
@ -728,10 +740,11 @@ class Exchange:
|
||||
f'Exchange {self._api.name} does not support fetching historical candlestick data.'
|
||||
f'Message: {e}') from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(f'Could not load ticker history due to {e.__class__.__name__}. '
|
||||
f'Message: {e}') from e
|
||||
raise TemporaryError(f'Could not load ticker history for pair {pair} due to '
|
||||
f'{e.__class__.__name__}. Message: {e}') from e
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(f'Could not fetch ticker data. Msg: {e}') from e
|
||||
raise OperationalException(f'Could not fetch ticker data for pair {pair}. '
|
||||
f'Msg: {e}') from e
|
||||
|
||||
@retrier_async
|
||||
async def _async_fetch_trades(self, pair: str,
|
||||
@ -976,8 +989,8 @@ class Exchange:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
@retrier
|
||||
def get_fee(self, symbol, type='', side='', amount=1,
|
||||
price=1, taker_or_maker='maker') -> float:
|
||||
def get_fee(self, symbol: str, type: str = '', side: str = '', amount: float = 1,
|
||||
price: float = 1, taker_or_maker: str = 'maker') -> float:
|
||||
try:
|
||||
# validate that markets are loaded before trying to get fee
|
||||
if self._api.markets is None or len(self._api.markets) == 0:
|
||||
@ -1000,7 +1013,7 @@ def get_exchange_bad_reason(exchange_name: str) -> str:
|
||||
return BAD_EXCHANGES.get(exchange_name, "")
|
||||
|
||||
|
||||
def is_exchange_known_ccxt(exchange_name: str, ccxt_module=None) -> bool:
|
||||
def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool:
|
||||
return exchange_name in ccxt_exchanges(ccxt_module)
|
||||
|
||||
|
||||
@ -1008,14 +1021,14 @@ def is_exchange_officially_supported(exchange_name: str) -> bool:
|
||||
return exchange_name in ['bittrex', 'binance']
|
||||
|
||||
|
||||
def ccxt_exchanges(ccxt_module=None) -> List[str]:
|
||||
def ccxt_exchanges(ccxt_module: CcxtModuleType = None) -> List[str]:
|
||||
"""
|
||||
Return the list of all exchanges known to ccxt
|
||||
"""
|
||||
return ccxt_module.exchanges if ccxt_module is not None else ccxt.exchanges
|
||||
|
||||
|
||||
def available_exchanges(ccxt_module=None) -> List[str]:
|
||||
def available_exchanges(ccxt_module: CcxtModuleType = None) -> List[str]:
|
||||
"""
|
||||
Return exchanges available to the bot, i.e. non-bad exchanges in the ccxt list
|
||||
"""
|
||||
@ -1075,7 +1088,8 @@ def timeframe_to_next_date(timeframe: str, date: datetime = None) -> datetime:
|
||||
return datetime.fromtimestamp(new_timestamp, tz=timezone.utc)
|
||||
|
||||
|
||||
def symbol_is_pair(market_symbol: str, base_currency: str = None, quote_currency: str = None):
|
||||
def symbol_is_pair(market_symbol: str, base_currency: str = None,
|
||||
quote_currency: str = None) -> bool:
|
||||
"""
|
||||
Check if the market symbol is a pair, i.e. that its symbol consists of the base currency and the
|
||||
quote currency separated by '/' character. If base_currency and/or quote_currency is passed,
|
||||
@ -1088,7 +1102,7 @@ def symbol_is_pair(market_symbol: str, base_currency: str = None, quote_currency
|
||||
(symbol_parts[1] == quote_currency if quote_currency else len(symbol_parts[1]) > 0))
|
||||
|
||||
|
||||
def market_is_active(market):
|
||||
def market_is_active(market: Dict) -> bool:
|
||||
"""
|
||||
Return True if the market is active.
|
||||
"""
|
||||
|
@ -4,7 +4,8 @@ from typing import Dict
|
||||
|
||||
import ccxt
|
||||
|
||||
from freqtrade.exceptions import OperationalException, TemporaryError
|
||||
from freqtrade.exceptions import (DependencyException, InvalidOrderException,
|
||||
OperationalException, TemporaryError)
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.exchange.exchange import retrier
|
||||
|
||||
@ -15,6 +16,7 @@ class Kraken(Exchange):
|
||||
|
||||
_params: Dict = {"trading_agreement": "agree"}
|
||||
_ft_has: Dict = {
|
||||
"stoploss_on_exchange": True,
|
||||
"trades_pagination": "id",
|
||||
"trades_pagination_arg": "since",
|
||||
}
|
||||
@ -48,3 +50,51 @@ class Kraken(Exchange):
|
||||
f'Could not get balance due to {e.__class__.__name__}. Message: {e}') from e
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool:
|
||||
"""
|
||||
Verify stop_loss against stoploss-order value (limit or price)
|
||||
Returns True if adjustment is necessary.
|
||||
"""
|
||||
return order['type'] == 'stop-loss' and stop_loss > float(order['price'])
|
||||
|
||||
def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict:
|
||||
"""
|
||||
Creates a stoploss market order.
|
||||
Stoploss market orders is the only stoploss type supported by kraken.
|
||||
"""
|
||||
|
||||
ordertype = "stop-loss"
|
||||
|
||||
stop_price = self.price_to_precision(pair, stop_price)
|
||||
|
||||
if self._config['dry_run']:
|
||||
dry_order = self.dry_run_order(
|
||||
pair, ordertype, "sell", amount, stop_price)
|
||||
return dry_order
|
||||
|
||||
try:
|
||||
params = self._params.copy()
|
||||
|
||||
amount = self.amount_to_precision(pair, amount)
|
||||
|
||||
order = self._api.create_order(symbol=pair, type=ordertype, side='sell',
|
||||
amount=amount, price=stop_price, params=params)
|
||||
logger.info('stoploss order added for %s. '
|
||||
'stop price: %s.', pair, stop_price)
|
||||
return order
|
||||
except ccxt.InsufficientFunds as e:
|
||||
raise DependencyException(
|
||||
f'Insufficient funds to create {ordertype} sell order on market {pair}.'
|
||||
f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. '
|
||||
f'Message: {e}') from e
|
||||
except ccxt.InvalidOrder as e:
|
||||
raise InvalidOrderException(
|
||||
f'Could not create {ordertype} sell order on market {pair}. '
|
||||
f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. '
|
||||
f'Message: {e}') from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
@ -265,7 +265,7 @@ class FreqtradeBot:
|
||||
|
||||
return used_rate
|
||||
|
||||
def get_trade_stake_amount(self, pair) -> float:
|
||||
def get_trade_stake_amount(self, pair: str) -> float:
|
||||
"""
|
||||
Calculate stake amount for the trade
|
||||
:return: float: Stake amount
|
||||
@ -427,23 +427,23 @@ class FreqtradeBot:
|
||||
Checks depth of market before executing a buy
|
||||
"""
|
||||
conf_bids_to_ask_delta = conf.get('bids_to_ask_delta', 0)
|
||||
logger.info('checking depth of market for %s', pair)
|
||||
logger.info(f"Checking depth of market for {pair} ...")
|
||||
order_book = self.exchange.get_order_book(pair, 1000)
|
||||
order_book_data_frame = order_book_to_dataframe(order_book['bids'], order_book['asks'])
|
||||
order_book_bids = order_book_data_frame['b_size'].sum()
|
||||
order_book_asks = order_book_data_frame['a_size'].sum()
|
||||
bids_ask_delta = order_book_bids / order_book_asks
|
||||
logger.info(
|
||||
f"bids: {order_book_bids}, asks: {order_book_asks}, delta: {bids_ask_delta}, "
|
||||
f"askprice: {order_book['asks'][0][0]}, bidprice: {order_book['bids'][0][0]}, "
|
||||
f"immediate ask quantity: {order_book['asks'][0][1]}, "
|
||||
f"immediate bid quantity: {order_book['bids'][0][1]}",
|
||||
f"Bids: {order_book_bids}, Asks: {order_book_asks}, Delta: {bids_ask_delta}, "
|
||||
f"Bid Price: {order_book['bids'][0][0]}, Ask Price: {order_book['asks'][0][0]}, "
|
||||
f"Immediate Bid Quantity: {order_book['bids'][0][1]}, "
|
||||
f"Immediate Ask Quantity: {order_book['asks'][0][1]}."
|
||||
)
|
||||
if bids_ask_delta >= conf_bids_to_ask_delta:
|
||||
logger.info('bids to ask delta DOES satisfy condition.')
|
||||
logger.info(f"Bids to asks delta for {pair} DOES satisfy condition.")
|
||||
return True
|
||||
else:
|
||||
logger.info(f"bids to ask delta for {pair} does not satisfy condition.")
|
||||
logger.info(f"Bids to asks delta for {pair} does not satisfy condition.")
|
||||
return False
|
||||
|
||||
def execute_buy(self, pair: str, stake_amount: float, price: Optional[float] = None) -> bool:
|
||||
@ -539,7 +539,7 @@ class FreqtradeBot:
|
||||
|
||||
return True
|
||||
|
||||
def _notify_buy(self, trade: Trade, order_type: str):
|
||||
def _notify_buy(self, trade: Trade, order_type: str) -> None:
|
||||
"""
|
||||
Sends rpc notification when a buy occured.
|
||||
"""
|
||||
@ -627,7 +627,7 @@ class FreqtradeBot:
|
||||
self.dataprovider.ohlcv(trade.pair, self.strategy.ticker_interval))
|
||||
|
||||
if config_ask_strategy.get('use_order_book', False):
|
||||
logger.info('Using order book for selling...')
|
||||
logger.debug(f'Using order book for selling {trade.pair}...')
|
||||
# logger.debug('Order book %s',orderBook)
|
||||
order_book_min = config_ask_strategy.get('order_book_min', 1)
|
||||
order_book_max = config_ask_strategy.get('order_book_max', 1)
|
||||
@ -636,7 +636,7 @@ class FreqtradeBot:
|
||||
|
||||
for i in range(order_book_min, order_book_max + 1):
|
||||
order_book_rate = order_book['asks'][i - 1][0]
|
||||
logger.info(' order book asks top %s: %0.8f', i, order_book_rate)
|
||||
logger.debug(' order book asks top %s: %0.8f', i, order_book_rate)
|
||||
sell_rate = order_book_rate
|
||||
|
||||
if self._check_and_execute_sell(trade, sell_rate, buy, sell):
|
||||
@ -658,13 +658,10 @@ class FreqtradeBot:
|
||||
Force-sells the pair (using EmergencySell reason) in case of Problems creating the order.
|
||||
:return: True if the order succeeded, and False in case of problems.
|
||||
"""
|
||||
# Limit price threshold: As limit price should always be below stop-price
|
||||
LIMIT_PRICE_PCT = self.strategy.order_types.get('stoploss_on_exchange_limit_ratio', 0.99)
|
||||
|
||||
try:
|
||||
stoploss_order = self.exchange.stoploss_limit(pair=trade.pair, amount=trade.amount,
|
||||
stop_price=stop_price,
|
||||
rate=rate * LIMIT_PRICE_PCT)
|
||||
stoploss_order = self.exchange.stoploss(pair=trade.pair, amount=trade.amount,
|
||||
stop_price=stop_price,
|
||||
order_types=self.strategy.order_types)
|
||||
trade.stoploss_order_id = str(stoploss_order['id'])
|
||||
return True
|
||||
except InvalidOrderException as e:
|
||||
@ -696,8 +693,24 @@ class FreqtradeBot:
|
||||
except InvalidOrderException as exception:
|
||||
logger.warning('Unable to fetch stoploss order: %s', exception)
|
||||
|
||||
# We check if stoploss order is fulfilled
|
||||
if stoploss_order and stoploss_order['status'] == 'closed':
|
||||
trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value
|
||||
trade.update(stoploss_order)
|
||||
# Lock pair for one candle to prevent immediate rebuys
|
||||
self.strategy.lock_pair(trade.pair,
|
||||
timeframe_to_next_date(self.config['ticker_interval']))
|
||||
self._notify_sell(trade, "stoploss")
|
||||
return True
|
||||
|
||||
if trade.open_order_id or not trade.is_open:
|
||||
# Trade has an open Buy or Sell order, Stoploss-handling can't happen in this case
|
||||
# as the Amount on the exchange is tied up in another trade.
|
||||
# The trade can be closed already (sell-order fill confirmation came in this iteration)
|
||||
return False
|
||||
|
||||
# If buy order is fulfilled but there is no stoploss, we add a stoploss on exchange
|
||||
if (not trade.open_order_id and not stoploss_order):
|
||||
if (not stoploss_order):
|
||||
|
||||
stoploss = self.edge.stoploss(pair=trade.pair) if self.edge else self.strategy.stoploss
|
||||
|
||||
@ -716,16 +729,6 @@ class FreqtradeBot:
|
||||
trade.stoploss_order_id = None
|
||||
logger.warning('Stoploss order was cancelled, but unable to recreate one.')
|
||||
|
||||
# We check if stoploss order is fulfilled
|
||||
if stoploss_order and stoploss_order['status'] == 'closed':
|
||||
trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value
|
||||
trade.update(stoploss_order)
|
||||
# Lock pair for one candle to prevent immediate rebuys
|
||||
self.strategy.lock_pair(trade.pair,
|
||||
timeframe_to_next_date(self.config['ticker_interval']))
|
||||
self._notify_sell(trade, "stoploss")
|
||||
return True
|
||||
|
||||
# Finally we check if stoploss on exchange should be moved up because of trailing.
|
||||
if stoploss_order and self.config.get('trailing_stop', False):
|
||||
# if trailing stoploss is enabled we check if stoploss value has changed
|
||||
@ -735,7 +738,7 @@ class FreqtradeBot:
|
||||
|
||||
return False
|
||||
|
||||
def handle_trailing_stoploss_on_exchange(self, trade: Trade, order):
|
||||
def handle_trailing_stoploss_on_exchange(self, trade: Trade, order: dict) -> None:
|
||||
"""
|
||||
Check to see if stoploss on exchange should be updated
|
||||
in case of trailing stoploss on exchange
|
||||
@ -743,8 +746,7 @@ class FreqtradeBot:
|
||||
:param order: Current on exchange stoploss order
|
||||
:return: None
|
||||
"""
|
||||
|
||||
if trade.stop_loss > float(order['info']['stopPrice']):
|
||||
if self.exchange.stoploss_adjust(trade.stop_loss, order):
|
||||
# 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:
|
||||
@ -758,10 +760,8 @@ class FreqtradeBot:
|
||||
f"for pair {trade.pair}")
|
||||
|
||||
# Create new stoploss order
|
||||
if self.create_stoploss_order(trade=trade, stop_price=trade.stop_loss,
|
||||
rate=trade.stop_loss):
|
||||
return False
|
||||
else:
|
||||
if not self.create_stoploss_order(trade=trade, stop_price=trade.stop_loss,
|
||||
rate=trade.stop_loss):
|
||||
logger.warning(f"Could not create trailing stoploss order "
|
||||
f"for pair {trade.pair}.")
|
||||
|
||||
@ -990,7 +990,7 @@ class FreqtradeBot:
|
||||
|
||||
self._notify_sell(trade, order_type)
|
||||
|
||||
def _notify_sell(self, trade: Trade, order_type: str):
|
||||
def _notify_sell(self, trade: Trade, order_type: str) -> None:
|
||||
"""
|
||||
Sends rpc notification when a sell occured.
|
||||
"""
|
||||
@ -1031,7 +1031,7 @@ class FreqtradeBot:
|
||||
# Common update trade state methods
|
||||
#
|
||||
|
||||
def update_trade_state(self, trade, action_order: dict = None):
|
||||
def update_trade_state(self, trade: Trade, action_order: dict = None) -> None:
|
||||
"""
|
||||
Checks trades with open orders and updates the amount if necessary
|
||||
"""
|
||||
|
@ -6,6 +6,7 @@ import logging
|
||||
import re
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from typing.io import IO
|
||||
|
||||
import numpy as np
|
||||
@ -40,7 +41,7 @@ def datesarray_to_datetimearray(dates: np.ndarray) -> np.ndarray:
|
||||
return dates.dt.to_pydatetime()
|
||||
|
||||
|
||||
def file_dump_json(filename: Path, data, is_zip=False) -> None:
|
||||
def file_dump_json(filename: Path, data: Any, is_zip: bool = False) -> None:
|
||||
"""
|
||||
Dump JSON data into a file
|
||||
:param filename: file to create
|
||||
@ -61,7 +62,7 @@ def file_dump_json(filename: Path, data, is_zip=False) -> None:
|
||||
logger.debug(f'done json to "{filename}"')
|
||||
|
||||
|
||||
def json_load(datafile: IO):
|
||||
def json_load(datafile: IO) -> Any:
|
||||
"""
|
||||
load data with rapidjson
|
||||
Use this to have a consistent experience,
|
||||
@ -125,11 +126,11 @@ def round_dict(d, n):
|
||||
return {k: (round(v, n) if isinstance(v, float) else v) for k, v in d.items()}
|
||||
|
||||
|
||||
def plural(num, singular: str, plural: str = None) -> str:
|
||||
def plural(num: float, singular: str, plural: str = None) -> str:
|
||||
return singular if (num == 1 or num == -1) else plural or singular + 's'
|
||||
|
||||
|
||||
def render_template(templatefile: str, arguments: dict = {}):
|
||||
def render_template(templatefile: str, arguments: dict = {}) -> str:
|
||||
|
||||
from jinja2 import Environment, PackageLoader, select_autoescape
|
||||
|
||||
|
@ -9,6 +9,7 @@ from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, NamedTuple, Optional
|
||||
|
||||
import arrow
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.configuration import (TimeRange, remove_credentials,
|
||||
@ -24,7 +25,7 @@ from freqtrade.optimize.optimize_reports import (
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
||||
from freqtrade.state import RunMode
|
||||
from freqtrade.strategy.interface import IStrategy, SellType
|
||||
from freqtrade.strategy.interface import IStrategy, SellCheckTuple, SellType
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -148,7 +149,7 @@ class Backtesting:
|
||||
logger.info(f'Dumping backtest results to {recordfilename}')
|
||||
file_dump_json(recordfilename, records)
|
||||
|
||||
def _get_ticker_list(self, processed) -> Dict[str, DataFrame]:
|
||||
def _get_ticker_list(self, processed: Dict) -> Dict[str, DataFrame]:
|
||||
"""
|
||||
Helper function to convert a processed tickerlist into a list for performance reasons.
|
||||
|
||||
@ -175,7 +176,8 @@ class Backtesting:
|
||||
ticker[pair] = [x for x in ticker_data.itertuples()]
|
||||
return ticker
|
||||
|
||||
def _get_close_rate(self, sell_row, trade: Trade, sell, trade_dur) -> float:
|
||||
def _get_close_rate(self, sell_row, trade: Trade, sell: SellCheckTuple,
|
||||
trade_dur: int) -> float:
|
||||
"""
|
||||
Get close rate for backtesting result
|
||||
"""
|
||||
@ -280,7 +282,7 @@ class Backtesting:
|
||||
return None
|
||||
|
||||
def backtest(self, processed: Dict, stake_amount: float,
|
||||
start_date, end_date,
|
||||
start_date: arrow.Arrow, end_date: arrow.Arrow,
|
||||
max_open_trades: int = 0, position_stacking: bool = False) -> DataFrame:
|
||||
"""
|
||||
Implement backtesting functionality
|
||||
@ -404,12 +406,12 @@ class Backtesting:
|
||||
)
|
||||
# Execute backtest and print results
|
||||
all_results[self.strategy.get_strategy_name()] = self.backtest(
|
||||
processed=preprocessed,
|
||||
stake_amount=self.config['stake_amount'],
|
||||
start_date=min_date,
|
||||
end_date=max_date,
|
||||
max_open_trades=max_open_trades,
|
||||
position_stacking=position_stacking,
|
||||
processed=preprocessed,
|
||||
stake_amount=self.config['stake_amount'],
|
||||
start_date=min_date,
|
||||
end_date=max_date,
|
||||
max_open_trades=max_open_trades,
|
||||
position_stacking=position_stacking,
|
||||
)
|
||||
|
||||
for strategy, results in all_results.items():
|
||||
@ -426,7 +428,10 @@ class Backtesting:
|
||||
results=results))
|
||||
|
||||
print(' SELL REASON STATS '.center(133, '='))
|
||||
print(generate_text_table_sell_reason(data, results))
|
||||
print(generate_text_table_sell_reason(data,
|
||||
stake_currency=self.config['stake_currency'],
|
||||
max_open_trades=self.config['max_open_trades'],
|
||||
results=results))
|
||||
|
||||
print(' LEFT OPEN TRADES REPORT '.center(133, '='))
|
||||
print(generate_text_table(data,
|
||||
|
@ -59,6 +59,7 @@ class Hyperopt:
|
||||
hyperopt = Hyperopt(config)
|
||||
hyperopt.start()
|
||||
"""
|
||||
|
||||
def __init__(self, config: Dict[str, Any]) -> None:
|
||||
self.config = config
|
||||
|
||||
@ -90,13 +91,13 @@ class Hyperopt:
|
||||
# Populate functions here (hasattr is slow so should not be run during "regular" operations)
|
||||
if hasattr(self.custom_hyperopt, 'populate_indicators'):
|
||||
self.backtesting.strategy.advise_indicators = \
|
||||
self.custom_hyperopt.populate_indicators # type: ignore
|
||||
self.custom_hyperopt.populate_indicators # type: ignore
|
||||
if hasattr(self.custom_hyperopt, 'populate_buy_trend'):
|
||||
self.backtesting.strategy.advise_buy = \
|
||||
self.custom_hyperopt.populate_buy_trend # type: ignore
|
||||
self.custom_hyperopt.populate_buy_trend # type: ignore
|
||||
if hasattr(self.custom_hyperopt, 'populate_sell_trend'):
|
||||
self.backtesting.strategy.advise_sell = \
|
||||
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
|
||||
if self.config.get('use_max_market_positions', True):
|
||||
@ -117,11 +118,11 @@ class Hyperopt:
|
||||
self.print_json = self.config.get('print_json', False)
|
||||
|
||||
@staticmethod
|
||||
def get_lock_filename(config) -> str:
|
||||
def get_lock_filename(config: Dict[str, Any]) -> str:
|
||||
|
||||
return str(config['user_data_dir'] / 'hyperopt.lock')
|
||||
|
||||
def clean_hyperopt(self):
|
||||
def clean_hyperopt(self) -> None:
|
||||
"""
|
||||
Remove hyperopt pickle files to restart hyperopt.
|
||||
"""
|
||||
@ -158,7 +159,7 @@ class Hyperopt:
|
||||
f"saved to '{self.trials_file}'.")
|
||||
|
||||
@staticmethod
|
||||
def _read_trials(trials_file) -> List:
|
||||
def _read_trials(trials_file: Path) -> List:
|
||||
"""
|
||||
Read hyperopt trials file
|
||||
"""
|
||||
@ -189,7 +190,7 @@ class Hyperopt:
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def print_epoch_details(results, total_epochs, print_json: bool,
|
||||
def print_epoch_details(results, total_epochs: int, print_json: bool,
|
||||
no_header: bool = False, header_str: str = None) -> None:
|
||||
"""
|
||||
Display details of the hyperopt result
|
||||
@ -218,7 +219,7 @@ class Hyperopt:
|
||||
Hyperopt._params_pretty_print(params, 'trailing', "Trailing stop:")
|
||||
|
||||
@staticmethod
|
||||
def _params_update_for_json(result_dict, params, space: str):
|
||||
def _params_update_for_json(result_dict, params, space: str) -> None:
|
||||
if space in params:
|
||||
space_params = Hyperopt._space_params(params, space)
|
||||
if space in ['buy', 'sell']:
|
||||
@ -235,7 +236,7 @@ class Hyperopt:
|
||||
result_dict.update(space_params)
|
||||
|
||||
@staticmethod
|
||||
def _params_pretty_print(params, space: str, header: str):
|
||||
def _params_pretty_print(params, space: str, header: str) -> None:
|
||||
if space in params:
|
||||
space_params = Hyperopt._space_params(params, space, 5)
|
||||
if space == 'stoploss':
|
||||
@ -251,7 +252,7 @@ class Hyperopt:
|
||||
return round_dict(d, r) if r else d
|
||||
|
||||
@staticmethod
|
||||
def is_best_loss(results, current_best_loss) -> bool:
|
||||
def is_best_loss(results, current_best_loss: float) -> bool:
|
||||
return results['loss'] < current_best_loss
|
||||
|
||||
def print_results(self, results) -> None:
|
||||
@ -345,15 +346,15 @@ class Hyperopt:
|
||||
|
||||
if self.has_space('roi'):
|
||||
self.backtesting.strategy.minimal_roi = \
|
||||
self.custom_hyperopt.generate_roi_table(params_dict)
|
||||
self.custom_hyperopt.generate_roi_table(params_dict)
|
||||
|
||||
if self.has_space('buy'):
|
||||
self.backtesting.strategy.advise_buy = \
|
||||
self.custom_hyperopt.buy_strategy_generator(params_dict)
|
||||
self.custom_hyperopt.buy_strategy_generator(params_dict)
|
||||
|
||||
if self.has_space('sell'):
|
||||
self.backtesting.strategy.advise_sell = \
|
||||
self.custom_hyperopt.sell_strategy_generator(params_dict)
|
||||
self.custom_hyperopt.sell_strategy_generator(params_dict)
|
||||
|
||||
if self.has_space('stoploss'):
|
||||
self.backtesting.strategy.stoploss = params_dict['stoploss']
|
||||
@ -372,12 +373,12 @@ class Hyperopt:
|
||||
min_date, max_date = get_timerange(processed)
|
||||
|
||||
backtesting_results = self.backtesting.backtest(
|
||||
processed=processed,
|
||||
stake_amount=self.config['stake_amount'],
|
||||
start_date=min_date,
|
||||
end_date=max_date,
|
||||
max_open_trades=self.max_open_trades,
|
||||
position_stacking=self.position_stacking,
|
||||
processed=processed,
|
||||
stake_amount=self.config['stake_amount'],
|
||||
start_date=min_date,
|
||||
end_date=max_date,
|
||||
max_open_trades=self.max_open_trades,
|
||||
position_stacking=self.position_stacking,
|
||||
)
|
||||
return self._get_results_dict(backtesting_results, min_date, max_date,
|
||||
params_dict, params_details)
|
||||
@ -438,7 +439,7 @@ class Hyperopt:
|
||||
random_state=self.random_state,
|
||||
)
|
||||
|
||||
def fix_optimizer_models_list(self):
|
||||
def fix_optimizer_models_list(self) -> None:
|
||||
"""
|
||||
WORKAROUND: Since skopt is not actively supported, this resolves problems with skopt
|
||||
memory usage, see also: https://github.com/scikit-optimize/scikit-optimize/pull/746
|
||||
@ -460,7 +461,7 @@ class Hyperopt:
|
||||
wrap_non_picklable_objects(self.generate_optimizer))(v, i) for v in asked)
|
||||
|
||||
@staticmethod
|
||||
def load_previous_results(trials_file) -> List:
|
||||
def load_previous_results(trials_file: Path) -> List:
|
||||
"""
|
||||
Load data for epochs from the file if we have one
|
||||
"""
|
||||
@ -469,8 +470,8 @@ class Hyperopt:
|
||||
trials = Hyperopt._read_trials(trials_file)
|
||||
if trials[0].get('is_best') is None:
|
||||
raise OperationalException(
|
||||
"The file with Hyperopt results is incompatible with this version "
|
||||
"of Freqtrade and cannot be loaded.")
|
||||
"The file with Hyperopt results is incompatible with this version "
|
||||
"of Freqtrade and cannot be loaded.")
|
||||
logger.info(f"Loaded {len(trials)} previous evaluations from disk.")
|
||||
return trials
|
||||
|
||||
|
@ -28,18 +28,19 @@ class SharpeHyperOptLoss(IHyperOptLoss):
|
||||
|
||||
Uses Sharpe Ratio calculation.
|
||||
"""
|
||||
total_profit = results.profit_percent
|
||||
total_profit = results["profit_percent"]
|
||||
days_period = (max_date - min_date).days
|
||||
|
||||
# adding slippage of 0.1% per trade
|
||||
total_profit = total_profit - 0.0005
|
||||
expected_yearly_return = total_profit.sum() / days_period
|
||||
expected_returns_mean = total_profit.sum() / days_period
|
||||
up_stdev = np.std(total_profit)
|
||||
|
||||
if (np.std(total_profit) != 0.):
|
||||
sharp_ratio = expected_yearly_return / np.std(total_profit) * np.sqrt(365)
|
||||
sharp_ratio = expected_returns_mean / up_stdev * np.sqrt(365)
|
||||
else:
|
||||
# Define high (negative) sharpe ratio to be clear that this is NOT optimal.
|
||||
sharp_ratio = -20.
|
||||
|
||||
# print(expected_yearly_return, np.std(total_profit), sharp_ratio)
|
||||
# print(expected_returns_mean, up_stdev, sharp_ratio)
|
||||
return -sharp_ratio
|
||||
|
@ -19,9 +19,17 @@ def generate_text_table(data: Dict[str, Dict], stake_currency: str, max_open_tra
|
||||
|
||||
floatfmt = ('s', 'd', '.2f', '.2f', '.8f', '.2f', 'd', '.1f', '.1f')
|
||||
tabular_data = []
|
||||
headers = ['pair', 'buy count', 'avg profit %', 'cum profit %',
|
||||
f'tot profit {stake_currency}', 'tot profit %', 'avg duration',
|
||||
'profit', 'loss']
|
||||
headers = [
|
||||
'Pair',
|
||||
'Buy Count',
|
||||
'Avg Profit %',
|
||||
'Cum Profit %',
|
||||
f'Tot Profit {stake_currency}',
|
||||
'Tot Profit %',
|
||||
'Avg Duration',
|
||||
'Wins',
|
||||
'Losses'
|
||||
]
|
||||
for pair in data:
|
||||
result = results[results.pair == pair]
|
||||
if skip_nan and result.profit_abs.isnull().all():
|
||||
@ -58,7 +66,9 @@ def generate_text_table(data: Dict[str, Dict], stake_currency: str, max_open_tra
|
||||
floatfmt=floatfmt, tablefmt="pipe") # type: ignore
|
||||
|
||||
|
||||
def generate_text_table_sell_reason(data: Dict[str, Dict], results: DataFrame) -> str:
|
||||
def generate_text_table_sell_reason(
|
||||
data: Dict[str, Dict], stake_currency: str, max_open_trades: int, results: DataFrame
|
||||
) -> str:
|
||||
"""
|
||||
Generate small table outlining Backtest results
|
||||
:param data: Dict of <pair: dataframe> containing data that was used during backtesting.
|
||||
@ -66,13 +76,36 @@ def generate_text_table_sell_reason(data: Dict[str, Dict], results: DataFrame) -
|
||||
:return: pretty printed table with tabulate as string
|
||||
"""
|
||||
tabular_data = []
|
||||
headers = ['Sell Reason', 'Count', 'Profit', 'Loss', 'Profit %']
|
||||
headers = [
|
||||
"Sell Reason",
|
||||
"Sell Count",
|
||||
"Wins",
|
||||
"Losses",
|
||||
"Avg Profit %",
|
||||
"Cum Profit %",
|
||||
f"Tot Profit {stake_currency}",
|
||||
"Tot Profit %",
|
||||
]
|
||||
for reason, count in results['sell_reason'].value_counts().iteritems():
|
||||
result = results.loc[results['sell_reason'] == reason]
|
||||
profit = len(result[result['profit_abs'] >= 0])
|
||||
loss = len(result[result['profit_abs'] < 0])
|
||||
profit_mean = round(result['profit_percent'].mean() * 100.0, 2)
|
||||
tabular_data.append([reason.value, count, profit, loss, profit_mean])
|
||||
profit_sum = round(result["profit_percent"].sum() * 100.0, 2)
|
||||
profit_tot = result['profit_abs'].sum()
|
||||
profit_percent_tot = round(result['profit_percent'].sum() * 100.0 / max_open_trades, 2)
|
||||
tabular_data.append(
|
||||
[
|
||||
reason.value,
|
||||
count,
|
||||
profit,
|
||||
loss,
|
||||
profit_mean,
|
||||
profit_sum,
|
||||
profit_tot,
|
||||
profit_percent_tot,
|
||||
]
|
||||
)
|
||||
return tabulate(tabular_data, headers=headers, tablefmt="pipe")
|
||||
|
||||
|
||||
|
@ -7,7 +7,7 @@ Provides lists as configured in config.json
|
||||
import logging
|
||||
from abc import ABC, abstractmethod, abstractproperty
|
||||
from copy import deepcopy
|
||||
from typing import Dict, List
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from freqtrade.exchange import market_is_active
|
||||
|
||||
@ -16,7 +16,8 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class IPairList(ABC):
|
||||
|
||||
def __init__(self, exchange, pairlistmanager, config, pairlistconfig: dict,
|
||||
def __init__(self, exchange, pairlistmanager,
|
||||
config: Dict[str, Any], pairlistconfig: Dict[str, Any],
|
||||
pairlist_pos: int) -> None:
|
||||
"""
|
||||
:param exchange: Exchange instance
|
||||
|
@ -48,10 +48,10 @@ class PrecisionFilter(IPairList):
|
||||
"""
|
||||
Filters and sorts pairlists and assigns and returns them again.
|
||||
"""
|
||||
stoploss = None
|
||||
if self._config.get('stoploss') is not None:
|
||||
stoploss = self._config.get('stoploss')
|
||||
if stoploss is not None:
|
||||
# Precalculate sanitized stoploss value to avoid recalculation for every pair
|
||||
stoploss = 1 - abs(self._config.get('stoploss'))
|
||||
stoploss = 1 - abs(stoploss)
|
||||
# Copy list since we're modifying this list
|
||||
for p in deepcopy(pairlist):
|
||||
ticker = tickers.get(p)
|
||||
|
@ -1,6 +1,6 @@
|
||||
import logging
|
||||
from copy import deepcopy
|
||||
from typing import Dict, List
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from freqtrade.pairlist.IPairList import IPairList
|
||||
|
||||
@ -9,7 +9,8 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class PriceFilter(IPairList):
|
||||
|
||||
def __init__(self, exchange, pairlistmanager, config, pairlistconfig: dict,
|
||||
def __init__(self, exchange, pairlistmanager,
|
||||
config: Dict[str, Any], pairlistconfig: Dict[str, Any],
|
||||
pairlist_pos: int) -> None:
|
||||
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
||||
|
||||
|
@ -6,7 +6,7 @@ Provides lists as configured in config.json
|
||||
"""
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Dict, List
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.pairlist.IPairList import IPairList
|
||||
@ -18,7 +18,7 @@ SORT_VALUES = ['askVolume', 'bidVolume', 'quoteVolume']
|
||||
|
||||
class VolumePairList(IPairList):
|
||||
|
||||
def __init__(self, exchange, pairlistmanager, config, pairlistconfig: dict,
|
||||
def __init__(self, exchange, pairlistmanager, config: Dict[str, Any], pairlistconfig: dict,
|
||||
pairlist_pos: int) -> None:
|
||||
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
||||
|
||||
@ -79,8 +79,8 @@ class VolumePairList(IPairList):
|
||||
else:
|
||||
return pairlist
|
||||
|
||||
def _gen_pair_whitelist(self, pairlist, tickers, base_currency: str,
|
||||
key: str, min_val: int) -> List[str]:
|
||||
def _gen_pair_whitelist(self, pairlist: List[str], tickers: Dict,
|
||||
base_currency: str, key: str) -> List[str]:
|
||||
"""
|
||||
Updates the whitelist with with a dynamically generated list
|
||||
:param base_currency: base currency as str
|
||||
|
@ -64,11 +64,11 @@ def init(db_url: str, clean_open_orders: bool = False) -> None:
|
||||
clean_dry_run_db()
|
||||
|
||||
|
||||
def has_column(columns, searchname: str) -> bool:
|
||||
def has_column(columns: List, searchname: str) -> bool:
|
||||
return len(list(filter(lambda x: x["name"] == searchname, columns))) == 1
|
||||
|
||||
|
||||
def get_column_def(columns, column: str, default: str) -> str:
|
||||
def get_column_def(columns: List, column: str, default: str) -> str:
|
||||
return default if not has_column(columns, column) else column
|
||||
|
||||
|
||||
@ -246,14 +246,15 @@ class Trade(_DECL_BASE):
|
||||
if self.initial_stop_loss_pct else None),
|
||||
}
|
||||
|
||||
def adjust_min_max_rates(self, current_price: float):
|
||||
def adjust_min_max_rates(self, current_price: float) -> None:
|
||||
"""
|
||||
Adjust the max_rate and min_rate.
|
||||
"""
|
||||
self.max_rate = max(current_price, self.max_rate or self.open_rate)
|
||||
self.min_rate = min(current_price, self.min_rate or self.open_rate)
|
||||
|
||||
def adjust_stop_loss(self, current_price: float, stoploss: float, initial: bool = False):
|
||||
def adjust_stop_loss(self, current_price: float, stoploss: float,
|
||||
initial: bool = False) -> None:
|
||||
"""
|
||||
This adjusts the stop loss to it's most recently observed setting
|
||||
:param current_price: Current rate the asset is traded
|
||||
|
@ -370,7 +370,7 @@ def generate_profit_graph(pairs: str, tickers: Dict[str, pd.DataFrame],
|
||||
return fig
|
||||
|
||||
|
||||
def generate_plot_filename(pair, timeframe) -> str:
|
||||
def generate_plot_filename(pair: str, timeframe: str) -> str:
|
||||
"""
|
||||
Generate filenames per pair/timeframe to be used for storing plots
|
||||
"""
|
||||
|
@ -25,7 +25,7 @@ class IResolver:
|
||||
initial_search_path: Path
|
||||
|
||||
@classmethod
|
||||
def build_search_paths(cls, config, user_subdir: Optional[str] = None,
|
||||
def build_search_paths(cls, config: Dict[str, Any], user_subdir: Optional[str] = None,
|
||||
extra_dir: Optional[str] = None) -> List[Path]:
|
||||
|
||||
abs_paths: List[Path] = [cls.initial_search_path]
|
||||
|
@ -9,7 +9,7 @@ from base64 import urlsafe_b64decode
|
||||
from collections import OrderedDict
|
||||
from inspect import getfullargspec
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from freqtrade.constants import (REQUIRED_ORDERTIF, REQUIRED_ORDERTYPES,
|
||||
USERPATH_STRATEGY)
|
||||
@ -30,7 +30,7 @@ class StrategyResolver(IResolver):
|
||||
initial_search_path = Path(__file__).parent.parent.joinpath('strategy').resolve()
|
||||
|
||||
@staticmethod
|
||||
def load_strategy(config: Optional[Dict] = None) -> IStrategy:
|
||||
def load_strategy(config: Dict[str, Any] = None) -> IStrategy:
|
||||
"""
|
||||
Load the custom class from config parameter
|
||||
:param config: configuration dictionary or None
|
||||
@ -96,7 +96,8 @@ class StrategyResolver(IResolver):
|
||||
return strategy
|
||||
|
||||
@staticmethod
|
||||
def _override_attribute_helper(strategy, config, attribute: str, default):
|
||||
def _override_attribute_helper(strategy, config: Dict[str, Any],
|
||||
attribute: str, default: Any):
|
||||
"""
|
||||
Override attributes in the strategy.
|
||||
Prevalence:
|
||||
|
@ -139,7 +139,8 @@ class RPC:
|
||||
results.append(trade_dict)
|
||||
return results
|
||||
|
||||
def _rpc_status_table(self, stake_currency, fiat_display_currency: str) -> Tuple[List, List]:
|
||||
def _rpc_status_table(self, stake_currency: str,
|
||||
fiat_display_currency: str) -> Tuple[List, List]:
|
||||
trades = Trade.get_open_trades()
|
||||
if not trades:
|
||||
raise RPCException('no active trade')
|
||||
@ -385,7 +386,7 @@ class RPC:
|
||||
|
||||
return {'status': 'No more buy will occur from now. Run /reload_conf to reset.'}
|
||||
|
||||
def _rpc_forcesell(self, trade_id) -> Dict[str, str]:
|
||||
def _rpc_forcesell(self, trade_id: str) -> Dict[str, str]:
|
||||
"""
|
||||
Handler for forcesell <id>.
|
||||
Sells the given trade at current price
|
||||
|
@ -61,7 +61,7 @@ class RPCManager:
|
||||
except NotImplementedError:
|
||||
logger.error(f"Message type {msg['type']} not implemented by handler {mod.name}.")
|
||||
|
||||
def startup_messages(self, config, pairlist) -> None:
|
||||
def startup_messages(self, config: Dict[str, Any], pairlist) -> None:
|
||||
if config['dry_run']:
|
||||
self.send_msg({
|
||||
'type': RPCMessageType.WARNING_NOTIFICATION,
|
||||
|
@ -180,7 +180,7 @@ class IStrategy(ABC):
|
||||
if pair not in self._pair_locked_until or self._pair_locked_until[pair] < until:
|
||||
self._pair_locked_until[pair] = until
|
||||
|
||||
def unlock_pair(self, pair) -> None:
|
||||
def unlock_pair(self, pair: str) -> None:
|
||||
"""
|
||||
Unlocks a pair previously locked using lock_pair.
|
||||
Not used by freqtrade itself, but intended to be used if users lock pairs
|
||||
|
4
freqtrade/vendor/qtpylib/indicators.py
vendored
4
freqtrade/vendor/qtpylib/indicators.py
vendored
@ -288,9 +288,9 @@ def rolling_min(series, window=14, min_periods=None):
|
||||
def rolling_max(series, window=14, min_periods=None):
|
||||
min_periods = window if min_periods is None else min_periods
|
||||
try:
|
||||
return series.rolling(window=window, min_periods=min_periods).min()
|
||||
return series.rolling(window=window, min_periods=min_periods).max()
|
||||
except Exception as e: # noqa: F841
|
||||
return pd.Series(series).rolling(window=window, min_periods=min_periods).min()
|
||||
return pd.Series(series).rolling(window=window, min_periods=min_periods).max()
|
||||
|
||||
|
||||
# ---------------------------------------------
|
||||
|
@ -30,24 +30,21 @@ class Wallets:
|
||||
self._last_wallet_refresh = 0
|
||||
self.update()
|
||||
|
||||
def get_free(self, currency) -> float:
|
||||
|
||||
def get_free(self, currency: str) -> float:
|
||||
balance = self._wallets.get(currency)
|
||||
if balance and balance.free:
|
||||
return balance.free
|
||||
else:
|
||||
return 0
|
||||
|
||||
def get_used(self, currency) -> float:
|
||||
|
||||
def get_used(self, currency: str) -> float:
|
||||
balance = self._wallets.get(currency)
|
||||
if balance and balance.used:
|
||||
return balance.used
|
||||
else:
|
||||
return 0
|
||||
|
||||
def get_total(self, currency) -> float:
|
||||
|
||||
def get_total(self, currency: str) -> float:
|
||||
balance = self._wallets.get(currency)
|
||||
if balance and balance.total:
|
||||
return balance.total
|
||||
@ -87,7 +84,6 @@ class Wallets:
|
||||
self._wallets = _wallets
|
||||
|
||||
def _update_live(self) -> None:
|
||||
|
||||
balances = self._exchange.get_balances()
|
||||
|
||||
for currency in balances:
|
||||
|
@ -22,7 +22,7 @@ class Worker:
|
||||
Freqtradebot worker class
|
||||
"""
|
||||
|
||||
def __init__(self, args: Dict[str, Any], config=None) -> None:
|
||||
def __init__(self, args: Dict[str, Any], config: Dict[str, Any] = None) -> None:
|
||||
"""
|
||||
Init all variables and objects the bot needs to work
|
||||
"""
|
||||
|
@ -1,6 +1,6 @@
|
||||
# requirements without requirements installable via conda
|
||||
# mainly used for Raspberry pi installs
|
||||
ccxt==1.21.91
|
||||
ccxt==1.22.30
|
||||
SQLAlchemy==1.3.13
|
||||
python-telegram-bot==12.3.0
|
||||
arrow==0.15.5
|
||||
@ -12,7 +12,7 @@ jsonschema==3.2.0
|
||||
TA-Lib==0.4.17
|
||||
tabulate==0.8.6
|
||||
coinmarketcap==5.0.3
|
||||
jinja2==2.10.3
|
||||
jinja2==2.11.1
|
||||
|
||||
# find first, C search in arrays
|
||||
py_find_1st==1.1.4
|
||||
|
@ -8,7 +8,7 @@ flake8==3.7.9
|
||||
flake8-type-annotations==0.1.0
|
||||
flake8-tidy-imports==4.0.0
|
||||
mypy==0.761
|
||||
pytest==5.3.4
|
||||
pytest==5.3.5
|
||||
pytest-asyncio==0.10.0
|
||||
pytest-cov==2.8.1
|
||||
pytest-mock==2.0.0
|
||||
|
@ -4,6 +4,6 @@
|
||||
# Required for hyperopt
|
||||
scipy==1.4.1
|
||||
scikit-learn==0.22.1
|
||||
scikit-optimize==0.5.2
|
||||
scikit-optimize==0.7.1
|
||||
filelock==3.0.12
|
||||
joblib==0.14.1
|
||||
|
@ -2,4 +2,4 @@
|
||||
-r requirements-common.txt
|
||||
|
||||
numpy==1.18.1
|
||||
pandas==0.25.3
|
||||
pandas==1.0.0
|
||||
|
@ -163,8 +163,8 @@ def test_edge_results(edge_conf, mocker, caplog, data) -> None:
|
||||
for c, trade in enumerate(data.trades):
|
||||
res = results.iloc[c]
|
||||
assert res.exit_type == trade.sell_reason
|
||||
assert arrow.get(res.open_time) == _get_frame_time_from_offset(trade.open_tick)
|
||||
assert arrow.get(res.close_time) == _get_frame_time_from_offset(trade.close_tick)
|
||||
assert res.open_time == np.datetime64(_get_frame_time_from_offset(trade.open_tick))
|
||||
assert res.close_time == np.datetime64(_get_frame_time_from_offset(trade.close_tick))
|
||||
|
||||
|
||||
def test_adjust(mocker, edge_conf):
|
||||
|
@ -9,7 +9,7 @@ from freqtrade.exceptions import (DependencyException, InvalidOrderException,
|
||||
from tests.conftest import get_patched_exchange
|
||||
|
||||
|
||||
def test_stoploss_limit_order(default_conf, mocker):
|
||||
def test_stoploss_order_binance(default_conf, mocker):
|
||||
api_mock = MagicMock()
|
||||
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
|
||||
order_type = 'stop_loss_limit'
|
||||
@ -28,46 +28,47 @@ def test_stoploss_limit_order(default_conf, mocker):
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
|
||||
|
||||
with pytest.raises(OperationalException):
|
||||
order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=190, rate=200)
|
||||
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190,
|
||||
order_types={'stoploss_on_exchange_limit_ratio': 1.05})
|
||||
|
||||
api_mock.create_order.reset_mock()
|
||||
|
||||
order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
|
||||
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||
|
||||
assert 'id' in order
|
||||
assert 'info' in order
|
||||
assert order['id'] == order_id
|
||||
assert api_mock.create_order.call_args[0][0] == 'ETH/BTC'
|
||||
assert api_mock.create_order.call_args[0][1] == order_type
|
||||
assert api_mock.create_order.call_args[0][2] == 'sell'
|
||||
assert api_mock.create_order.call_args[0][3] == 1
|
||||
assert api_mock.create_order.call_args[0][4] == 200
|
||||
assert api_mock.create_order.call_args[0][5] == {'stopPrice': 220}
|
||||
assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC'
|
||||
assert api_mock.create_order.call_args_list[0][1]['type'] == order_type
|
||||
assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell'
|
||||
assert api_mock.create_order.call_args_list[0][1]['amount'] == 1
|
||||
assert api_mock.create_order.call_args_list[0][1]['price'] == 220
|
||||
assert api_mock.create_order.call_args_list[0][1]['params'] == {'stopPrice': 220}
|
||||
|
||||
# test exception handling
|
||||
with pytest.raises(DependencyException):
|
||||
api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance"))
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
|
||||
exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
|
||||
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||
|
||||
with pytest.raises(InvalidOrderException):
|
||||
api_mock.create_order = MagicMock(
|
||||
side_effect=ccxt.InvalidOrder("binance Order would trigger immediately."))
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
|
||||
exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
|
||||
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||
|
||||
with pytest.raises(TemporaryError):
|
||||
api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("No connection"))
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
|
||||
exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
|
||||
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||
|
||||
with pytest.raises(OperationalException, match=r".*DeadBeef.*"):
|
||||
api_mock.create_order = MagicMock(side_effect=ccxt.BaseError("DeadBeef"))
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
|
||||
exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
|
||||
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||
|
||||
|
||||
def test_stoploss_limit_order_dry_run(default_conf, mocker):
|
||||
def test_stoploss_order_dry_run_binance(default_conf, mocker):
|
||||
api_mock = MagicMock()
|
||||
order_type = 'stop_loss_limit'
|
||||
default_conf['dry_run'] = True
|
||||
@ -77,11 +78,12 @@ def test_stoploss_limit_order_dry_run(default_conf, mocker):
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
|
||||
|
||||
with pytest.raises(OperationalException):
|
||||
order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=190, rate=200)
|
||||
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190,
|
||||
order_types={'stoploss_on_exchange_limit_ratio': 1.05})
|
||||
|
||||
api_mock.create_order.reset_mock()
|
||||
|
||||
order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
|
||||
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||
|
||||
assert 'id' in order
|
||||
assert 'info' in order
|
||||
@ -90,3 +92,17 @@ def test_stoploss_limit_order_dry_run(default_conf, mocker):
|
||||
assert order['type'] == order_type
|
||||
assert order['price'] == 220
|
||||
assert order['amount'] == 1
|
||||
|
||||
|
||||
def test_stoploss_adjust_binance(mocker, default_conf):
|
||||
exchange = get_patched_exchange(mocker, default_conf, id='binance')
|
||||
order = {
|
||||
'type': 'stop_loss_limit',
|
||||
'price': 1500,
|
||||
'info': {'stopPrice': 1500},
|
||||
}
|
||||
assert exchange.stoploss_adjust(1501, order)
|
||||
assert not exchange.stoploss_adjust(1499, order)
|
||||
# Test with invalid order case
|
||||
order['type'] = 'stop_loss'
|
||||
assert not exchange.stoploss_adjust(1501, order)
|
||||
|
@ -76,9 +76,11 @@ def test_init_ccxt_kwargs(default_conf, mocker, caplog):
|
||||
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
|
||||
caplog.set_level(logging.INFO)
|
||||
conf = copy.deepcopy(default_conf)
|
||||
conf['exchange']['ccxt_async_config'] = {'aiohttp_trust_env': True}
|
||||
conf['exchange']['ccxt_async_config'] = {'aiohttp_trust_env': True, 'asyncio_loop': True}
|
||||
ex = Exchange(conf)
|
||||
assert log_has("Applying additional ccxt config: {'aiohttp_trust_env': True}", caplog)
|
||||
assert log_has(
|
||||
"Applying additional ccxt config: {'aiohttp_trust_env': True, 'asyncio_loop': True}",
|
||||
caplog)
|
||||
assert ex._api_async.aiohttp_trust_env
|
||||
assert not ex._api.aiohttp_trust_env
|
||||
|
||||
@ -86,6 +88,8 @@ def test_init_ccxt_kwargs(default_conf, mocker, caplog):
|
||||
caplog.clear()
|
||||
conf = copy.deepcopy(default_conf)
|
||||
conf['exchange']['ccxt_config'] = {'TestKWARG': 11}
|
||||
conf['exchange']['ccxt_async_config'] = {'asyncio_loop': True}
|
||||
|
||||
ex = Exchange(conf)
|
||||
assert not log_has("Applying additional ccxt config: {'aiohttp_trust_env': True}", caplog)
|
||||
assert not ex._api_async.aiohttp_trust_env
|
||||
@ -1758,10 +1762,13 @@ def test_get_fee(default_conf, mocker, exchange_name):
|
||||
'get_fee', 'calculate_fee', symbol="ETH/BTC")
|
||||
|
||||
|
||||
def test_stoploss_limit_order_unsupported_exchange(default_conf, mocker):
|
||||
def test_stoploss_order_unsupported_exchange(default_conf, mocker):
|
||||
exchange = get_patched_exchange(mocker, default_conf, 'bittrex')
|
||||
with pytest.raises(OperationalException, match=r"stoploss_limit is not implemented .*"):
|
||||
exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
|
||||
with pytest.raises(OperationalException, match=r"stoploss is not implemented .*"):
|
||||
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||
|
||||
with pytest.raises(OperationalException, match=r"stoploss is not implemented .*"):
|
||||
exchange.stoploss_adjust(1, {})
|
||||
|
||||
|
||||
def test_merge_ft_has_dict(default_conf, mocker):
|
||||
|
@ -3,6 +3,11 @@
|
||||
from random import randint
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import ccxt
|
||||
import pytest
|
||||
|
||||
from freqtrade.exceptions import (DependencyException, InvalidOrderException,
|
||||
OperationalException, TemporaryError)
|
||||
from tests.conftest import get_patched_exchange
|
||||
from tests.exchange.test_exchange import ccxt_exceptionhandlers
|
||||
|
||||
@ -149,3 +154,98 @@ def test_get_balances_prod(default_conf, mocker):
|
||||
assert balances['4ST']['used'] == 0.0
|
||||
ccxt_exceptionhandlers(mocker, default_conf, api_mock, "kraken",
|
||||
"get_balances", "fetch_balance")
|
||||
|
||||
|
||||
def test_stoploss_order_kraken(default_conf, mocker):
|
||||
api_mock = MagicMock()
|
||||
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
|
||||
order_type = 'stop-loss'
|
||||
|
||||
api_mock.create_order = MagicMock(return_value={
|
||||
'id': order_id,
|
||||
'info': {
|
||||
'foo': 'bar'
|
||||
}
|
||||
})
|
||||
|
||||
default_conf['dry_run'] = False
|
||||
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
|
||||
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
|
||||
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken')
|
||||
|
||||
# stoploss_on_exchange_limit_ratio is irrelevant for kraken market orders
|
||||
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190,
|
||||
order_types={'stoploss_on_exchange_limit_ratio': 1.05})
|
||||
assert api_mock.create_order.call_count == 1
|
||||
|
||||
api_mock.create_order.reset_mock()
|
||||
|
||||
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||
|
||||
assert 'id' in order
|
||||
assert 'info' in order
|
||||
assert order['id'] == order_id
|
||||
assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC'
|
||||
assert api_mock.create_order.call_args_list[0][1]['type'] == order_type
|
||||
assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell'
|
||||
assert api_mock.create_order.call_args_list[0][1]['amount'] == 1
|
||||
assert api_mock.create_order.call_args_list[0][1]['price'] == 220
|
||||
assert api_mock.create_order.call_args_list[0][1]['params'] == {'trading_agreement': 'agree'}
|
||||
|
||||
# test exception handling
|
||||
with pytest.raises(DependencyException):
|
||||
api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance"))
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken')
|
||||
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||
|
||||
with pytest.raises(InvalidOrderException):
|
||||
api_mock.create_order = MagicMock(
|
||||
side_effect=ccxt.InvalidOrder("kraken Order would trigger immediately."))
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken')
|
||||
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||
|
||||
with pytest.raises(TemporaryError):
|
||||
api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("No connection"))
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken')
|
||||
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||
|
||||
with pytest.raises(OperationalException, match=r".*DeadBeef.*"):
|
||||
api_mock.create_order = MagicMock(side_effect=ccxt.BaseError("DeadBeef"))
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken')
|
||||
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||
|
||||
|
||||
def test_stoploss_order_dry_run_kraken(default_conf, mocker):
|
||||
api_mock = MagicMock()
|
||||
order_type = 'stop-loss'
|
||||
default_conf['dry_run'] = True
|
||||
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
|
||||
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
|
||||
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken')
|
||||
|
||||
api_mock.create_order.reset_mock()
|
||||
|
||||
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||
|
||||
assert 'id' in order
|
||||
assert 'info' in order
|
||||
assert 'type' in order
|
||||
|
||||
assert order['type'] == order_type
|
||||
assert order['price'] == 220
|
||||
assert order['amount'] == 1
|
||||
|
||||
|
||||
def test_stoploss_adjust_kraken(mocker, default_conf):
|
||||
exchange = get_patched_exchange(mocker, default_conf, id='kraken')
|
||||
order = {
|
||||
'type': 'stop-loss',
|
||||
'price': 1500,
|
||||
}
|
||||
assert exchange.stoploss_adjust(1501, order)
|
||||
assert not exchange.stoploss_adjust(1499, order)
|
||||
# Test with invalid order case ...
|
||||
order['type'] = 'stop_loss_limit'
|
||||
assert not exchange.stoploss_adjust(1501, order)
|
||||
|
@ -21,14 +21,14 @@ def test_generate_text_table(default_conf, mocker):
|
||||
)
|
||||
|
||||
result_str = (
|
||||
'| pair | buy count | avg profit % | cum profit % | '
|
||||
'tot profit BTC | tot profit % | avg duration | profit | loss |\n'
|
||||
'|:--------|------------:|---------------:|---------------:|'
|
||||
'-----------------:|---------------:|:---------------|---------:|-------:|\n'
|
||||
'| ETH/BTC | 2 | 15.00 | 30.00 | '
|
||||
'0.60000000 | 15.00 | 0:20:00 | 2 | 0 |\n'
|
||||
'| TOTAL | 2 | 15.00 | 30.00 | '
|
||||
'0.60000000 | 15.00 | 0:20:00 | 2 | 0 |'
|
||||
'| Pair | Buy Count | Avg Profit % | Cum Profit % | Tot Profit BTC '
|
||||
'| Tot Profit % | Avg Duration | Wins | Losses |\n'
|
||||
'|:--------|------------:|---------------:|---------------:|-----------------:'
|
||||
'|---------------:|:---------------|-------:|---------:|\n'
|
||||
'| ETH/BTC | 2 | 15.00 | 30.00 | 0.60000000 '
|
||||
'| 15.00 | 0:20:00 | 2 | 0 |\n'
|
||||
'| TOTAL | 2 | 15.00 | 30.00 | 0.60000000 '
|
||||
'| 15.00 | 0:20:00 | 2 | 0 |'
|
||||
)
|
||||
assert generate_text_table(data={'ETH/BTC': {}},
|
||||
stake_currency='BTC', max_open_trades=2,
|
||||
@ -50,13 +50,19 @@ def test_generate_text_table_sell_reason(default_conf, mocker):
|
||||
)
|
||||
|
||||
result_str = (
|
||||
'| Sell Reason | Count | Profit | Loss | Profit % |\n'
|
||||
'|:--------------|--------:|---------:|-------:|-----------:|\n'
|
||||
'| roi | 2 | 2 | 0 | 15 |\n'
|
||||
'| stop_loss | 1 | 0 | 1 | -10 |'
|
||||
'| Sell Reason | Sell Count | Wins | Losses | Avg Profit % |'
|
||||
' Cum Profit % | Tot Profit BTC | Tot Profit % |\n'
|
||||
'|:--------------|-------------:|-------:|---------:|---------------:|'
|
||||
'---------------:|-----------------:|---------------:|\n'
|
||||
'| roi | 2 | 2 | 0 | 15 |'
|
||||
' 30 | 0.6 | 15 |\n'
|
||||
'| stop_loss | 1 | 0 | 1 | -10 |'
|
||||
' -10 | -0.2 | -5 |'
|
||||
)
|
||||
assert generate_text_table_sell_reason(
|
||||
data={'ETH/BTC': {}}, results=results) == result_str
|
||||
data={'ETH/BTC': {}},
|
||||
stake_currency='BTC', max_open_trades=2,
|
||||
results=results) == result_str
|
||||
|
||||
|
||||
def test_generate_text_table_strategy(default_conf, mocker):
|
||||
|
@ -1023,8 +1023,8 @@ def test_add_stoploss_on_exchange(mocker, default_conf, limit_buy_order) -> None
|
||||
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount',
|
||||
return_value=limit_buy_order['amount'])
|
||||
|
||||
stoploss_limit = MagicMock(return_value={'id': 13434334})
|
||||
mocker.patch('freqtrade.exchange.Exchange.stoploss_limit', stoploss_limit)
|
||||
stoploss = MagicMock(return_value={'id': 13434334})
|
||||
mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss)
|
||||
|
||||
freqtrade = FreqtradeBot(default_conf)
|
||||
freqtrade.strategy.order_types['stoploss_on_exchange'] = True
|
||||
@ -1037,13 +1037,13 @@ def test_add_stoploss_on_exchange(mocker, default_conf, limit_buy_order) -> None
|
||||
|
||||
freqtrade.exit_positions(trades)
|
||||
assert trade.stoploss_order_id == '13434334'
|
||||
assert stoploss_limit.call_count == 1
|
||||
assert stoploss.call_count == 1
|
||||
assert trade.is_open is True
|
||||
|
||||
|
||||
def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
|
||||
limit_buy_order, limit_sell_order) -> None:
|
||||
stoploss_limit = MagicMock(return_value={'id': 13434334})
|
||||
stoploss = MagicMock(return_value={'id': 13434334})
|
||||
patch_RPCManager(mocker)
|
||||
patch_exchange(mocker)
|
||||
mocker.patch.multiple(
|
||||
@ -1056,7 +1056,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
|
||||
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
||||
sell=MagicMock(return_value={'id': limit_sell_order['id']}),
|
||||
get_fee=fee,
|
||||
stoploss_limit=stoploss_limit
|
||||
stoploss=stoploss
|
||||
)
|
||||
freqtrade = FreqtradeBot(default_conf)
|
||||
patch_get_signal(freqtrade)
|
||||
@ -1070,7 +1070,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
|
||||
trade.stoploss_order_id = None
|
||||
|
||||
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||
assert stoploss_limit.call_count == 1
|
||||
assert stoploss.call_count == 1
|
||||
assert trade.stoploss_order_id == "13434334"
|
||||
|
||||
# Second case: when stoploss is set but it is not yet hit
|
||||
@ -1094,10 +1094,10 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
|
||||
|
||||
canceled_stoploss_order = MagicMock(return_value={'status': 'canceled'})
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_order', canceled_stoploss_order)
|
||||
stoploss_limit.reset_mock()
|
||||
stoploss.reset_mock()
|
||||
|
||||
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||
assert stoploss_limit.call_count == 1
|
||||
assert stoploss.call_count == 1
|
||||
assert trade.stoploss_order_id == "13434334"
|
||||
|
||||
# Fourth case: when stoploss is set and it is hit
|
||||
@ -1124,9 +1124,10 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
|
||||
assert trade.is_open is False
|
||||
|
||||
mocker.patch(
|
||||
'freqtrade.exchange.Exchange.stoploss_limit',
|
||||
'freqtrade.exchange.Exchange.stoploss',
|
||||
side_effect=DependencyException()
|
||||
)
|
||||
trade.is_open = True
|
||||
freqtrade.handle_stoploss_on_exchange(trade)
|
||||
assert log_has('Unable to place a stoploss order on exchange.', caplog)
|
||||
assert trade.stoploss_order_id is None
|
||||
@ -1134,11 +1135,21 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
|
||||
# Fifth case: get_order returns InvalidOrder
|
||||
# It should try to add stoploss order
|
||||
trade.stoploss_order_id = 100
|
||||
stoploss_limit.reset_mock()
|
||||
stoploss.reset_mock()
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_order', side_effect=InvalidOrderException())
|
||||
mocker.patch('freqtrade.exchange.Exchange.stoploss_limit', stoploss_limit)
|
||||
mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss)
|
||||
freqtrade.handle_stoploss_on_exchange(trade)
|
||||
assert stoploss_limit.call_count == 1
|
||||
assert stoploss.call_count == 1
|
||||
|
||||
# Sixth case: Closed Trade
|
||||
# Should not create new order
|
||||
trade.stoploss_order_id = None
|
||||
trade.is_open = False
|
||||
stoploss.reset_mock()
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_order')
|
||||
mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss)
|
||||
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||
assert stoploss.call_count == 0
|
||||
|
||||
|
||||
def test_handle_sle_cancel_cant_recreate(mocker, default_conf, fee, caplog,
|
||||
@ -1157,7 +1168,7 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf, fee, caplog,
|
||||
sell=MagicMock(return_value={'id': limit_sell_order['id']}),
|
||||
get_fee=fee,
|
||||
get_order=MagicMock(return_value={'status': 'canceled'}),
|
||||
stoploss_limit=MagicMock(side_effect=DependencyException()),
|
||||
stoploss=MagicMock(side_effect=DependencyException()),
|
||||
)
|
||||
freqtrade = FreqtradeBot(default_conf)
|
||||
patch_get_signal(freqtrade)
|
||||
@ -1165,7 +1176,7 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf, fee, caplog,
|
||||
freqtrade.enter_positions()
|
||||
trade = Trade.query.first()
|
||||
trade.is_open = True
|
||||
trade.open_order_id = '12345'
|
||||
trade.open_order_id = None
|
||||
trade.stoploss_order_id = 100
|
||||
assert trade
|
||||
|
||||
@ -1191,7 +1202,7 @@ def test_create_stoploss_order_invalid_order(mocker, default_conf, caplog, fee,
|
||||
sell=sell_mock,
|
||||
get_fee=fee,
|
||||
get_order=MagicMock(return_value={'status': 'canceled'}),
|
||||
stoploss_limit=MagicMock(side_effect=InvalidOrderException()),
|
||||
stoploss=MagicMock(side_effect=InvalidOrderException()),
|
||||
)
|
||||
freqtrade = FreqtradeBot(default_conf)
|
||||
patch_get_signal(freqtrade)
|
||||
@ -1221,7 +1232,7 @@ def test_create_stoploss_order_invalid_order(mocker, default_conf, caplog, fee,
|
||||
def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog,
|
||||
limit_buy_order, limit_sell_order) -> None:
|
||||
# When trailing stoploss is set
|
||||
stoploss_limit = MagicMock(return_value={'id': 13434334})
|
||||
stoploss = MagicMock(return_value={'id': 13434334})
|
||||
patch_RPCManager(mocker)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
@ -1233,7 +1244,8 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog,
|
||||
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
||||
sell=MagicMock(return_value={'id': limit_sell_order['id']}),
|
||||
get_fee=fee,
|
||||
stoploss_limit=stoploss_limit
|
||||
stoploss=stoploss,
|
||||
stoploss_adjust=MagicMock(return_value=True),
|
||||
)
|
||||
|
||||
# enabling TSL
|
||||
@ -1288,7 +1300,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog,
|
||||
cancel_order_mock = MagicMock()
|
||||
stoploss_order_mock = MagicMock()
|
||||
mocker.patch('freqtrade.exchange.Exchange.cancel_order', cancel_order_mock)
|
||||
mocker.patch('freqtrade.exchange.Exchange.stoploss_limit', stoploss_order_mock)
|
||||
mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss_order_mock)
|
||||
|
||||
# stoploss should not be updated as the interval is 60 seconds
|
||||
assert freqtrade.handle_trade(trade) is False
|
||||
@ -1307,7 +1319,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog,
|
||||
cancel_order_mock.assert_called_once_with(100, 'ETH/BTC')
|
||||
stoploss_order_mock.assert_called_once_with(amount=85.25149190110828,
|
||||
pair='ETH/BTC',
|
||||
rate=0.00002344 * 0.95 * 0.99,
|
||||
order_types=freqtrade.strategy.order_types,
|
||||
stop_price=0.00002344 * 0.95)
|
||||
|
||||
# price fell below stoploss, so dry-run sells trade.
|
||||
@ -1322,7 +1334,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog,
|
||||
def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, caplog,
|
||||
limit_buy_order, limit_sell_order) -> None:
|
||||
# When trailing stoploss is set
|
||||
stoploss_limit = MagicMock(return_value={'id': 13434334})
|
||||
stoploss = MagicMock(return_value={'id': 13434334})
|
||||
patch_exchange(mocker)
|
||||
|
||||
mocker.patch.multiple(
|
||||
@ -1335,7 +1347,8 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c
|
||||
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
||||
sell=MagicMock(return_value={'id': limit_sell_order['id']}),
|
||||
get_fee=fee,
|
||||
stoploss_limit=stoploss_limit
|
||||
stoploss=stoploss,
|
||||
stoploss_adjust=MagicMock(return_value=True),
|
||||
)
|
||||
|
||||
# enabling TSL
|
||||
@ -1375,12 +1388,12 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c
|
||||
assert log_has_re(r"Could not cancel stoploss order abcd for pair ETH/BTC.*", caplog)
|
||||
|
||||
# Still try to create order
|
||||
assert stoploss_limit.call_count == 1
|
||||
assert stoploss.call_count == 1
|
||||
|
||||
# Fail creating stoploss order
|
||||
caplog.clear()
|
||||
cancel_mock = mocker.patch("freqtrade.exchange.Exchange.cancel_order", MagicMock())
|
||||
mocker.patch("freqtrade.exchange.Exchange.stoploss_limit", side_effect=DependencyException())
|
||||
mocker.patch("freqtrade.exchange.Exchange.stoploss", side_effect=DependencyException())
|
||||
freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging)
|
||||
assert cancel_mock.call_count == 1
|
||||
assert log_has_re(r"Could not create trailing stoploss order for pair ETH/BTC\..*", caplog)
|
||||
@ -1390,12 +1403,13 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog,
|
||||
limit_buy_order, limit_sell_order) -> None:
|
||||
|
||||
# When trailing stoploss is set
|
||||
stoploss_limit = MagicMock(return_value={'id': 13434334})
|
||||
stoploss = MagicMock(return_value={'id': 13434334})
|
||||
patch_RPCManager(mocker)
|
||||
patch_exchange(mocker)
|
||||
patch_edge(mocker)
|
||||
edge_conf['max_open_trades'] = float('inf')
|
||||
edge_conf['dry_run_wallet'] = 999.9
|
||||
edge_conf['exchange']['name'] = 'binance'
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
fetch_ticker=MagicMock(return_value={
|
||||
@ -1406,7 +1420,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog,
|
||||
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
||||
sell=MagicMock(return_value={'id': limit_sell_order['id']}),
|
||||
get_fee=fee,
|
||||
stoploss_limit=stoploss_limit
|
||||
stoploss=stoploss,
|
||||
)
|
||||
|
||||
# enabling TSL
|
||||
@ -1459,7 +1473,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog,
|
||||
cancel_order_mock = MagicMock()
|
||||
stoploss_order_mock = MagicMock()
|
||||
mocker.patch('freqtrade.exchange.Exchange.cancel_order', cancel_order_mock)
|
||||
mocker.patch('freqtrade.exchange.Exchange.stoploss_limit', stoploss_order_mock)
|
||||
mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss_order_mock)
|
||||
|
||||
# price goes down 5%
|
||||
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={
|
||||
@ -1492,7 +1506,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog,
|
||||
cancel_order_mock.assert_called_once_with(100, 'NEO/BTC')
|
||||
stoploss_order_mock.assert_called_once_with(amount=2131074.168797954,
|
||||
pair='NEO/BTC',
|
||||
rate=0.00002344 * 0.99 * 0.99,
|
||||
order_types=freqtrade.strategy.order_types,
|
||||
stop_price=0.00002344 * 0.99)
|
||||
|
||||
|
||||
@ -2423,7 +2437,7 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, ticker, fee, ticke
|
||||
default_conf['exchange']['name'] = 'binance'
|
||||
rpc_mock = patch_RPCManager(mocker)
|
||||
patch_exchange(mocker)
|
||||
stoploss_limit = MagicMock(return_value={
|
||||
stoploss = MagicMock(return_value={
|
||||
'id': 123,
|
||||
'info': {
|
||||
'foo': 'bar'
|
||||
@ -2437,7 +2451,7 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, ticker, fee, ticke
|
||||
get_fee=fee,
|
||||
amount_to_precision=lambda s, x, y: y,
|
||||
price_to_precision=lambda s, x, y: y,
|
||||
stoploss_limit=stoploss_limit,
|
||||
stoploss=stoploss,
|
||||
cancel_order=cancel_order,
|
||||
)
|
||||
|
||||
@ -2482,14 +2496,14 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, ticker, f
|
||||
price_to_precision=lambda s, x, y: y,
|
||||
)
|
||||
|
||||
stoploss_limit = MagicMock(return_value={
|
||||
stoploss = MagicMock(return_value={
|
||||
'id': 123,
|
||||
'info': {
|
||||
'foo': 'bar'
|
||||
}
|
||||
})
|
||||
|
||||
mocker.patch('freqtrade.exchange.Binance.stoploss_limit', stoploss_limit)
|
||||
mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss)
|
||||
|
||||
freqtrade = FreqtradeBot(default_conf)
|
||||
freqtrade.strategy.order_types['stoploss_on_exchange'] = True
|
||||
@ -2507,7 +2521,7 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, ticker, f
|
||||
# Assuming stoploss on exchnage is hit
|
||||
# stoploss_order_id should become None
|
||||
# and trade should be sold at the price of stoploss
|
||||
stoploss_limit_executed = MagicMock(return_value={
|
||||
stoploss_executed = MagicMock(return_value={
|
||||
"id": "123",
|
||||
"timestamp": 1542707426845,
|
||||
"datetime": "2018-11-20T09:50:26.845Z",
|
||||
@ -2525,7 +2539,7 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, ticker, f
|
||||
"fee": None,
|
||||
"trades": None
|
||||
})
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_limit_executed)
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_executed)
|
||||
|
||||
freqtrade.exit_positions(trades)
|
||||
assert trade.stoploss_order_id is None
|
||||
|
@ -20,7 +20,7 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee,
|
||||
default_conf['max_open_trades'] = 3
|
||||
default_conf['exchange']['name'] = 'binance'
|
||||
|
||||
stoploss_limit = {
|
||||
stoploss = {
|
||||
'id': 123,
|
||||
'info': {}
|
||||
}
|
||||
@ -53,7 +53,7 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee,
|
||||
SellCheckTuple(sell_flag=True, sell_type=SellType.SELL_SIGNAL)]
|
||||
)
|
||||
cancel_order_mock = MagicMock()
|
||||
mocker.patch('freqtrade.exchange.Binance.stoploss_limit', stoploss_limit)
|
||||
mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
fetch_ticker=ticker,
|
||||
|
Loading…
Reference in New Issue
Block a user