mirror of
https://github.com/freqtrade/freqtrade.git
synced 2024-11-10 10:21:59 +00:00
Merge branch 'develop' into feature_keyval_storage
This commit is contained in:
commit
854bd9af2b
9
.github/workflows/ci.yml
vendored
9
.github/workflows/ci.yml
vendored
|
@ -78,11 +78,13 @@ jobs:
|
|||
# Allow failure for coveralls
|
||||
coveralls || true
|
||||
|
||||
- name: Backtesting
|
||||
- name: Backtesting (multi)
|
||||
run: |
|
||||
cp config_examples/config_bittrex.example.json config.json
|
||||
freqtrade create-userdir --userdir user_data
|
||||
freqtrade backtesting --datadir tests/testdata --strategy SampleStrategy
|
||||
freqtrade new-strategy -s AwesomeStrategy
|
||||
freqtrade new-strategy -s AwesomeStrategyMin --template minimal
|
||||
freqtrade backtesting --datadir tests/testdata --strategy-list AwesomeStrategy AwesomeStrategyMin -i 5m
|
||||
|
||||
- name: Hyperopt
|
||||
run: |
|
||||
|
@ -164,7 +166,8 @@ jobs:
|
|||
run: |
|
||||
cp config_examples/config_bittrex.example.json config.json
|
||||
freqtrade create-userdir --userdir user_data
|
||||
freqtrade backtesting --datadir tests/testdata --strategy SampleStrategy
|
||||
freqtrade new-strategy -s AwesomeStrategyAdv --template advanced
|
||||
freqtrade backtesting --datadir tests/testdata --strategy AwesomeStrategyAdv
|
||||
|
||||
- name: Hyperopt
|
||||
run: |
|
||||
|
|
|
@ -14,10 +14,10 @@ repos:
|
|||
exclude: build_helpers
|
||||
additional_dependencies:
|
||||
- types-cachetools==5.0.1
|
||||
- types-filelock==3.2.5
|
||||
- types-requests==2.27.25
|
||||
- types-filelock==3.2.6
|
||||
- types-requests==2.27.29
|
||||
- types-tabulate==0.8.9
|
||||
- types-python-dateutil==2.8.15
|
||||
- types-python-dateutil==2.8.17
|
||||
# stages: [push]
|
||||
|
||||
- repo: https://github.com/pycqa/isort
|
||||
|
|
|
@ -9,10 +9,6 @@ Freqtrade is a free and open source crypto trading bot written in Python. It is
|
|||
|
||||
![freqtrade](https://raw.githubusercontent.com/freqtrade/freqtrade/develop/docs/assets/freqtrade-screenshot.png)
|
||||
|
||||
## Sponsored promotion
|
||||
|
||||
[![tokenbot-promo](https://raw.githubusercontent.com/freqtrade/freqtrade/develop/docs/assets/TokenBot-Freqtrade-banner.png)](https://tokenbot.com/?utm_source=github&utm_medium=freqtrade&utm_campaign=algodevs)
|
||||
|
||||
## Disclaimer
|
||||
|
||||
This software is for educational purposes only. Do not risk money which
|
||||
|
@ -39,7 +35,7 @@ Please read the [exchange specific notes](docs/exchanges.md) to learn about even
|
|||
- [X] [OKX](https://okx.com/) (Former OKEX)
|
||||
- [ ] [potentially many others](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
|
||||
|
||||
### Experimentally, freqtrade also supports futures on the following exchanges
|
||||
### Supported Futures Exchanges (experimental)
|
||||
|
||||
- [X] [Binance](https://www.binance.com/)
|
||||
- [X] [Gate.io](https://www.gate.io/ref/6266643)
|
||||
|
|
|
@ -530,8 +530,9 @@ Since backtesting lacks some detailed information about what happens within a ca
|
|||
- Exit-reason does not explain if a trade was positive or negative, just what triggered the exit (this can look odd if negative ROI values are used)
|
||||
- Evaluation sequence (if multiple signals happen on the same candle)
|
||||
- Exit-signal
|
||||
- ROI (if not stoploss)
|
||||
- Stoploss
|
||||
- ROI
|
||||
- Trailing stoploss
|
||||
|
||||
Taking these assumptions, backtesting tries to mirror real trading as closely as possible. However, backtesting will **never** replace running a strategy in dry-run mode.
|
||||
Also, keep in mind that past results don't guarantee future success.
|
||||
|
|
|
@ -140,7 +140,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
|||
| `dry_run` | **Required.** Define if the bot must be in Dry Run or production mode. <br>*Defaults to `true`.* <br> **Datatype:** Boolean
|
||||
| `dry_run_wallet` | Define the starting amount in stake currency for the simulated wallet used by the bot running in Dry Run mode.<br>*Defaults to `1000`.* <br> **Datatype:** Float
|
||||
| `cancel_open_orders_on_exit` | Cancel open orders when the `/stop` RPC command is issued, `Ctrl+C` is pressed or the bot dies unexpectedly. When set to `true`, this allows you to use `/stop` to cancel unfilled and partially filled orders in the event of a market crash. It does not impact open positions. <br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
||||
| `process_only_new_candles` | Enable processing of indicators only when new candles arrive. If false each loop populates the indicators, this will mean the same candle is processed many times creating system load but can be useful of your strategy depends on tick data not only candle. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
||||
| `process_only_new_candles` | Enable processing of indicators only when new candles arrive. If false each loop populates the indicators, this will mean the same candle is processed many times creating system load but can be useful of your strategy depends on tick data not only candle. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `true`.* <br> **Datatype:** Boolean
|
||||
| `minimal_roi` | **Required.** Set the threshold as ratio the bot will use to exit a trade. [More information below](#understand-minimal_roi). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Dict
|
||||
| `stoploss` | **Required.** Value as ratio of the stoploss used by the bot. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Float (as ratio)
|
||||
| `trailing_stop` | Enables trailing stoploss (based on `stoploss` in either configuration or strategy file). More details in the [stoploss documentation](stoploss.md#trailing-stop-loss). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Boolean
|
||||
|
@ -230,6 +230,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
|||
| `dataformat_trades` | Data format to use to store historical trades data. <br> *Defaults to `jsongz`*. <br> **Datatype:** String
|
||||
| `position_adjustment_enable` | Enables the strategy to use position adjustments (additional buys or sells). [More information here](strategy-callbacks.md#adjust-trade-position). <br> [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.*<br> **Datatype:** Boolean
|
||||
| `max_entry_position_adjustment` | Maximum additional order(s) for each open trade on top of the first entry Order. Set it to `-1` for unlimited additional orders. [More information here](strategy-callbacks.md#adjust-trade-position). <br> [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `-1`.*<br> **Datatype:** Positive Integer or -1
|
||||
| `futures_funding_rate` | User-specified funding rate to be used when historical funding rates are not available from the exchange. This does not overwrite real historical rates. It is recommended that this be set to 0 unless you are testing a specific coin and you understand how the funding rate will affect freqtrade's profit calculations. [More information here](leverage.md#unavailable-funding-rates) <br>*Defaults to None.*<br> **Datatype:** Float
|
||||
|
||||
### Parameters in the strategy
|
||||
|
||||
|
@ -583,7 +584,7 @@ Once you will be happy with your bot performance running in the Dry-run mode, yo
|
|||
* Market orders fill based on orderbook volume the moment the order is placed.
|
||||
* Limit orders fill once the price reaches the defined level - or time out based on `unfilledtimeout` settings.
|
||||
* In combination with `stoploss_on_exchange`, the stop_loss price is assumed to be filled.
|
||||
* Open orders (not trades, which are stored in the database) are reset on bot restart.
|
||||
* Open orders (not trades, which are stored in the database) are kept open after bot restarts, with the assumption that they were not filled while being offline.
|
||||
|
||||
## Switch to production mode
|
||||
|
||||
|
|
|
@ -314,6 +314,32 @@ The output will show the last entry from the Exchange as well as the current UTC
|
|||
If the day shows the same day, then the last candle can be assumed as incomplete and should be dropped (leave the setting `"ohlcv_partial_candle"` from the exchange-class untouched / True). Otherwise, set `"ohlcv_partial_candle"` to `False` to not drop Candles (shown in the example above).
|
||||
Another way is to run this command multiple times in a row and observe if the volume is changing (while the date remains the same).
|
||||
|
||||
### Update binance cached leverage tiers
|
||||
|
||||
Updating leveraged tiers should be done regularly - and requires an authenticated account with futures enabled.
|
||||
|
||||
``` python
|
||||
import ccxt
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
exchange = ccxt.binance({
|
||||
'apiKey': '<apikey>',
|
||||
'secret': '<secret>'
|
||||
'options': {'defaultType': 'future'}
|
||||
})
|
||||
_ = exchange.load_markets()
|
||||
|
||||
lev_tiers = exchange.fetch_leverage_tiers()
|
||||
|
||||
# Assumes this is running in the root of the repository.
|
||||
file = Path('freqtrade/exchange/binance_leverage_tiers.json')
|
||||
json.dump(lev_tiers, file.open('w'), indent=2)
|
||||
|
||||
```
|
||||
|
||||
This file should then be contributed upstream, so others can benefit from this, too.
|
||||
|
||||
## Updating example notebooks
|
||||
|
||||
To keep the jupyter notebooks aligned with the documentation, the following should be ran after updating a example notebook.
|
||||
|
|
|
@ -680,7 +680,7 @@ class MyAwesomeStrategy(IStrategy):
|
|||
|
||||
!!! Note
|
||||
Values in the configuration file will overwrite Parameter-file level parameters - and both will overwrite parameters within the strategy.
|
||||
The prevalence is therefore: config > parameter file > strategy
|
||||
The prevalence is therefore: config > parameter file > strategy `*_params` > parameter default
|
||||
|
||||
### Understand Hyperopt ROI results
|
||||
|
||||
|
|
|
@ -22,10 +22,6 @@ Freqtrade is a free and open source crypto trading bot written in Python. It is
|
|||
|
||||
![freqtrade screenshot](assets/freqtrade-screenshot.png)
|
||||
|
||||
## Sponsored promotion
|
||||
|
||||
[![tokenbot-promo](assets/TokenBot-Freqtrade-banner.png)](https://tokenbot.com/?utm_source=github&utm_medium=freqtrade&utm_campaign=algodevs)
|
||||
|
||||
## Features
|
||||
|
||||
- Develop your Strategy: Write your strategy in python, using [pandas](https://pandas.pydata.org/). Example strategies to inspire you are available in the [strategy repository](https://github.com/freqtrade/freqtrade-strategies).
|
||||
|
@ -51,7 +47,7 @@ Please read the [exchange specific notes](exchanges.md) to learn about eventual,
|
|||
- [X] [OKX](https://okx.com/) (Former OKEX)
|
||||
- [ ] [potentially many others through <img alt="ccxt" width="30px" src="assets/ccxt-logo.svg" />](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
|
||||
|
||||
### Experimentally, freqtrade also supports futures on the following exchanges:
|
||||
### Supported Futures Exchanges (experimental)
|
||||
|
||||
- [X] [Binance](https://www.binance.com/)
|
||||
- [X] [Gate.io](https://www.gate.io/ref/6266643)
|
||||
|
|
|
@ -101,6 +101,13 @@ Possible values are any floats between 0.0 and 0.99
|
|||
!!! Danger "A `liquidation_buffer` of 0.0, or a low `liquidation_buffer` is likely to result in liquidations, and liquidation fees"
|
||||
Currently Freqtrade is able to calculate liquidation prices, but does not calculate liquidation fees. Setting your `liquidation_buffer` to 0.0, or using a low `liquidation_buffer` could result in your positions being liquidated. Freqtrade does not track liquidation fees, so liquidations will result in inaccurate profit/loss results for your bot. If you use a low `liquidation_buffer`, it is recommended to use `stoploss_on_exchange` if your exchange supports this.
|
||||
|
||||
## Unavailable funding rates
|
||||
|
||||
For futures data, exchanges commonly provide the futures candles, the marks, and the funding rates. However, it is common that whilst candles and marks might be available, the funding rates are not. This can affect backtesting timeranges, i.e. you may only be able to test recent timeranges and not earlier, experiencing the `No data found. Terminating.` error. To get around this, add the `futures_funding_rate` config option as listed in [configuration.md](configuration.md), and it is recommended that you set this to `0`, unless you know a given specific funding rate for your pair, exchange and timerange. Setting this to anything other than `0` can have drastic effects on your profit calculations within strategy, e.g. within the `custom_exit`, `custom_stoploss`, etc functions.
|
||||
|
||||
!!! Warning "This will mean your backtests are inaccurate."
|
||||
This will not overwrite funding rates that are available from the exchange, but bear in mind that setting a false funding rate will mean backtesting results will be inaccurate for historical timeranges where funding rates are not available.
|
||||
|
||||
### Developer
|
||||
|
||||
#### Margin mode
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
mkdocs==1.3.0
|
||||
mkdocs-material==8.2.15
|
||||
mkdocs-material==8.2.16
|
||||
mdx_truly_sane_lists==1.2
|
||||
pymdown-extensions==9.4
|
||||
jinja2==3.1.2
|
||||
|
|
|
@ -46,6 +46,9 @@ class AwesomeStrategy(IStrategy):
|
|||
self.cust_remote_data = requests.get('https://some_remote_source.example.com')
|
||||
|
||||
```
|
||||
|
||||
During hyperopt, this runs only once at startup.
|
||||
|
||||
## Bot loop start
|
||||
|
||||
A simple callback which is called once at the start of every bot throttling iteration (roughly every 5 seconds, unless configured differently).
|
||||
|
@ -563,6 +566,14 @@ class AwesomeStrategy(IStrategy):
|
|||
|
||||
`confirm_trade_exit()` can be used to abort a trade exit (sell) at the latest second (maybe because the price is not what we expect).
|
||||
|
||||
`confirm_trade_exit()` may be called multiple times within one iteration for the same trade if different exit-reasons apply.
|
||||
The exit-reasons (if applicable) will be in the following sequence:
|
||||
|
||||
* `exit_signal` / `custom_exit`
|
||||
* `stop_loss`
|
||||
* `roi`
|
||||
* `trailing_stop_loss`
|
||||
|
||||
``` python
|
||||
from freqtrade.persistence import Trade
|
||||
|
||||
|
@ -605,6 +616,9 @@ class AwesomeStrategy(IStrategy):
|
|||
|
||||
```
|
||||
|
||||
!!! Warning
|
||||
`confirm_trade_exit()` can prevent stoploss exits, causing significant losses as this would ignore stoploss exits.
|
||||
|
||||
## Adjust trade position
|
||||
|
||||
The `position_adjustment_enable` strategy property enables the usage of `adjust_trade_position()` callback in the strategy.
|
||||
|
@ -656,7 +670,7 @@ class DigDeeperStrategy(IStrategy):
|
|||
|
||||
# This is called when placing the initial order (opening trade)
|
||||
def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
|
||||
proposed_stake: float, min_stake: float, max_stake: float,
|
||||
proposed_stake: float, min_stake: Optional[float], max_stake: float,
|
||||
entry_tag: Optional[str], side: str, **kwargs) -> float:
|
||||
|
||||
# We need to leave most of the funds for possible further DCA orders
|
||||
|
@ -664,7 +678,7 @@ class DigDeeperStrategy(IStrategy):
|
|||
return proposed_stake / self.max_dca_multiplier
|
||||
|
||||
def adjust_trade_position(self, trade: Trade, current_time: datetime,
|
||||
current_rate: float, current_profit: float, min_stake: float,
|
||||
current_rate: float, current_profit: float, min_stake: Optional[float],
|
||||
max_stake: float, **kwargs):
|
||||
"""
|
||||
Custom trade adjustment logic, returning the stake amount that a trade should be increased.
|
||||
|
|
|
@ -199,7 +199,7 @@ New string argument `side` - which can be either `"long"` or `"short"`.
|
|||
``` python hl_lines="4"
|
||||
class AwesomeStrategy(IStrategy):
|
||||
def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
|
||||
proposed_stake: float, min_stake: float, max_stake: float,
|
||||
proposed_stake: float, min_stake: Optional[float], max_stake: float,
|
||||
entry_tag: Optional[str], **kwargs) -> float:
|
||||
# ...
|
||||
return proposed_stake
|
||||
|
@ -208,7 +208,7 @@ class AwesomeStrategy(IStrategy):
|
|||
``` python hl_lines="4"
|
||||
class AwesomeStrategy(IStrategy):
|
||||
def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
|
||||
proposed_stake: float, min_stake: float, max_stake: float,
|
||||
proposed_stake: float, min_stake: Optional[float], max_stake: float,
|
||||
entry_tag: Optional[str], side: str, **kwargs) -> float:
|
||||
# ...
|
||||
return proposed_stake
|
||||
|
|
|
@ -19,9 +19,9 @@ def start_convert_db(args: Dict[str, Any]) -> None:
|
|||
|
||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||
|
||||
init_db(config['db_url'], False)
|
||||
init_db(config['db_url'])
|
||||
session_target = Trade._session
|
||||
init_db(config['db_url_from'], False)
|
||||
init_db(config['db_url_from'])
|
||||
logger.info("Starting db migration.")
|
||||
|
||||
trade_count = 0
|
||||
|
|
|
@ -212,7 +212,7 @@ def start_show_trades(args: Dict[str, Any]) -> None:
|
|||
raise OperationalException("--db-url is required for this command.")
|
||||
|
||||
logger.info(f'Using DB: "{parse_db_uri_for_logging(config["db_url"])}"')
|
||||
init_db(config['db_url'], clean_open_orders=False)
|
||||
init_db(config['db_url'])
|
||||
tfilter = []
|
||||
|
||||
if config.get('trade_ids'):
|
||||
|
|
|
@ -27,7 +27,7 @@ def check_exchange(config: Dict[str, Any], check_for_bad: bool = True) -> bool:
|
|||
return True
|
||||
logger.info("Checking exchange...")
|
||||
|
||||
exchange = config.get('exchange', {}).get('name').lower()
|
||||
exchange = config.get('exchange', {}).get('name', '').lower()
|
||||
if not exchange:
|
||||
raise OperationalException(
|
||||
f'This command requires a configured exchange. You should either use '
|
||||
|
|
|
@ -490,7 +490,8 @@ class Configuration:
|
|||
if not pairs_file.exists():
|
||||
raise OperationalException(f'No pairs file found with path "{pairs_file}".')
|
||||
config['pairs'] = load_file(pairs_file)
|
||||
config['pairs'].sort()
|
||||
if isinstance(config['pairs'], list):
|
||||
config['pairs'].sort()
|
||||
return
|
||||
|
||||
if 'config' in self.args and self.args['config']:
|
||||
|
@ -501,5 +502,5 @@ class Configuration:
|
|||
pairs_file = config['datadir'] / 'pairs.json'
|
||||
if pairs_file.exists():
|
||||
config['pairs'] = load_file(pairs_file)
|
||||
if 'pairs' in config:
|
||||
if 'pairs' in config and isinstance(config['pairs'], list):
|
||||
config['pairs'].sort()
|
||||
|
|
|
@ -113,7 +113,7 @@ def process_temporary_deprecated_settings(config: Dict[str, Any]) -> None:
|
|||
process_removed_setting(config, 'experimental', 'ignore_roi_if_buy_signal',
|
||||
None, 'ignore_roi_if_entry_signal')
|
||||
|
||||
process_removed_setting(config, 'ask_strategy', 'use_sell_signal', None, 'exit_sell_signal')
|
||||
process_removed_setting(config, 'ask_strategy', 'use_sell_signal', None, 'use_exit_signal')
|
||||
process_removed_setting(config, 'ask_strategy', 'sell_profit_only', None, 'exit_profit_only')
|
||||
process_removed_setting(config, 'ask_strategy', 'sell_profit_offset',
|
||||
None, 'exit_profit_offset')
|
||||
|
|
|
@ -15,7 +15,7 @@ def create_datadir(config: Dict[str, Any], datadir: Optional[str] = None) -> Pat
|
|||
folder = Path(datadir) if datadir else Path(f"{config['user_data_dir']}/data")
|
||||
if not datadir:
|
||||
# set datadir
|
||||
exchange_name = config.get('exchange', {}).get('name').lower()
|
||||
exchange_name = config.get('exchange', {}).get('name', '').lower()
|
||||
folder = folder.joinpath(exchange_name)
|
||||
|
||||
if not folder.is_dir():
|
||||
|
|
|
@ -302,12 +302,12 @@ CONF_SCHEMA = {
|
|||
'exit_fill': {
|
||||
'type': 'string',
|
||||
'enum': TELEGRAM_SETTING_OPTIONS,
|
||||
'default': 'off'
|
||||
'default': 'on'
|
||||
},
|
||||
'protection_trigger': {
|
||||
'type': 'string',
|
||||
'enum': TELEGRAM_SETTING_OPTIONS,
|
||||
'default': 'off'
|
||||
'default': 'on'
|
||||
},
|
||||
'protection_trigger_global': {
|
||||
'type': 'string',
|
||||
|
|
|
@ -353,7 +353,7 @@ def load_trades_from_db(db_url: str, strategy: Optional[str] = None) -> pd.DataF
|
|||
Can also serve as protection to load the correct result.
|
||||
:return: Dataframe containing Trades
|
||||
"""
|
||||
init_db(db_url, clean_open_orders=False)
|
||||
init_db(db_url)
|
||||
|
||||
filters = []
|
||||
if strategy:
|
||||
|
|
|
@ -68,7 +68,8 @@ def load_data(datadir: Path,
|
|||
startup_candles: int = 0,
|
||||
fail_without_data: bool = False,
|
||||
data_format: str = 'json',
|
||||
candle_type: CandleType = CandleType.SPOT
|
||||
candle_type: CandleType = CandleType.SPOT,
|
||||
user_futures_funding_rate: int = None,
|
||||
) -> Dict[str, DataFrame]:
|
||||
"""
|
||||
Load ohlcv history data for a list of pairs.
|
||||
|
@ -100,6 +101,10 @@ def load_data(datadir: Path,
|
|||
)
|
||||
if not hist.empty:
|
||||
result[pair] = hist
|
||||
else:
|
||||
if candle_type is CandleType.FUNDING_RATE and user_futures_funding_rate is not None:
|
||||
logger.warn(f"{pair} using user specified [{user_futures_funding_rate}]")
|
||||
result[pair] = DataFrame(columns=["open", "close", "high", "low", "volume"])
|
||||
|
||||
if fail_without_data and not result:
|
||||
raise OperationalException("No data found. Terminating.")
|
||||
|
@ -277,6 +282,7 @@ def refresh_backtest_ohlcv_data(exchange: Exchange, pairs: List[str], timeframes
|
|||
pairs_not_available = []
|
||||
data_handler = get_datahandler(datadir, data_format)
|
||||
candle_type = CandleType.get_default(trading_mode)
|
||||
process = ''
|
||||
for idx, pair in enumerate(pairs, start=1):
|
||||
if pair not in exchange.markets:
|
||||
pairs_not_available.append(pair)
|
||||
|
|
|
@ -15,3 +15,9 @@ class ExitCheckTuple:
|
|||
@property
|
||||
def exit_flag(self):
|
||||
return self.exit_type != ExitType.NONE
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.exit_type == other.exit_type and self.exit_reason == other.exit_reason
|
||||
|
||||
def __repr__(self):
|
||||
return f"ExitCheckTuple({self.exit_type}, {self.exit_reason})"
|
||||
|
|
|
@ -53,11 +53,11 @@ class Binance(Exchange):
|
|||
ordertype = 'stop' if self.trading_mode == TradingMode.FUTURES else 'stop_loss_limit'
|
||||
|
||||
return order['type'] == ordertype and (
|
||||
(side == "sell" and stop_loss > float(order['info']['stopPrice'])) or
|
||||
(side == "buy" and stop_loss < float(order['info']['stopPrice']))
|
||||
(side == "sell" and stop_loss > float(order['stopPrice'])) or
|
||||
(side == "buy" and stop_loss < float(order['stopPrice']))
|
||||
)
|
||||
|
||||
def get_tickers(self, symbols: List[str] = None, cached: bool = False) -> Dict:
|
||||
def get_tickers(self, symbols: Optional[List[str]] = None, cached: bool = False) -> Dict:
|
||||
tickers = super().get_tickers(symbols=symbols, cached=cached)
|
||||
if self.trading_mode == TradingMode.FUTURES:
|
||||
# Binance's future result has no bid/ask values.
|
||||
|
@ -95,7 +95,7 @@ class Binance(Exchange):
|
|||
async def _async_get_historic_ohlcv(self, pair: str, timeframe: str,
|
||||
since_ms: int, candle_type: CandleType,
|
||||
is_new_pair: bool = False, raise_: bool = False,
|
||||
until_ms: int = None
|
||||
until_ms: Optional[int] = None
|
||||
) -> Tuple[str, str, str, List]:
|
||||
"""
|
||||
Overwrite to introduce "fast new pair" functionality by detecting the pair's listing date
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -29,3 +29,17 @@ class Bybit(Exchange):
|
|||
# (TradingMode.FUTURES, MarginMode.CROSS),
|
||||
# (TradingMode.FUTURES, MarginMode.ISOLATED)
|
||||
]
|
||||
|
||||
@property
|
||||
def _ccxt_config(self) -> Dict:
|
||||
# Parameters to add directly to ccxt sync/async initialization.
|
||||
# ccxt defaults to swap mode.
|
||||
config = {}
|
||||
if self.trading_mode == TradingMode.SPOT:
|
||||
config.update({
|
||||
"options": {
|
||||
"defaultType": "spot"
|
||||
}
|
||||
})
|
||||
config.update(super()._ccxt_config)
|
||||
return config
|
||||
|
|
|
@ -2,6 +2,7 @@ import asyncio
|
|||
import logging
|
||||
import time
|
||||
from functools import wraps
|
||||
from typing import Any, Callable, Optional, TypeVar, cast, overload
|
||||
|
||||
from freqtrade.exceptions import DDosProtection, RetryableOrderError, TemporaryError
|
||||
from freqtrade.mixins import LoggingMixin
|
||||
|
@ -11,6 +12,14 @@ logger = logging.getLogger(__name__)
|
|||
__logging_mixin = None
|
||||
|
||||
|
||||
def _reset_logging_mixin():
|
||||
"""
|
||||
Reset global logging mixin - used in tests only.
|
||||
"""
|
||||
global __logging_mixin
|
||||
__logging_mixin = LoggingMixin(logger)
|
||||
|
||||
|
||||
def _get_logging_mixin():
|
||||
# Logging-mixin to cache kucoin responses
|
||||
# Only to be used in retrier
|
||||
|
@ -133,8 +142,22 @@ def retrier_async(f):
|
|||
return wrapper
|
||||
|
||||
|
||||
def retrier(_func=None, retries=API_RETRY_COUNT):
|
||||
def decorator(f):
|
||||
F = TypeVar('F', bound=Callable[..., Any])
|
||||
|
||||
|
||||
# Type shenanigans
|
||||
@overload
|
||||
def retrier(_func: F) -> F:
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
def retrier(*, retries=API_RETRY_COUNT) -> Callable[[F], F]:
|
||||
...
|
||||
|
||||
|
||||
def retrier(_func: Optional[F] = None, *, retries=API_RETRY_COUNT):
|
||||
def decorator(f: F) -> F:
|
||||
@wraps(f)
|
||||
def wrapper(*args, **kwargs):
|
||||
count = kwargs.pop('count', retries)
|
||||
|
@ -155,7 +178,7 @@ def retrier(_func=None, retries=API_RETRY_COUNT):
|
|||
else:
|
||||
logger.warning(msg + 'Giving up.')
|
||||
raise ex
|
||||
return wrapper
|
||||
return cast(F, wrapper)
|
||||
# Support both @retrier and @retrier(retries=2) syntax
|
||||
if _func is None:
|
||||
return decorator
|
||||
|
|
|
@ -92,8 +92,8 @@ class Exchange:
|
|||
it does basic validation whether the specified exchange and pairs are valid.
|
||||
:return: None
|
||||
"""
|
||||
self._api: ccxt.Exchange = None
|
||||
self._api_async: ccxt_async.Exchange = None
|
||||
self._api: ccxt.Exchange
|
||||
self._api_async: ccxt_async.Exchange
|
||||
self._markets: Dict = {}
|
||||
self._trading_fees: Dict[str, Any] = {}
|
||||
self._leverage_tiers: Dict[str, List[Dict]] = {}
|
||||
|
@ -291,7 +291,7 @@ class Exchange:
|
|||
return self._markets
|
||||
|
||||
@property
|
||||
def precisionMode(self) -> str:
|
||||
def precisionMode(self) -> int:
|
||||
"""exchange ccxt precisionMode"""
|
||||
return self._api.precisionMode
|
||||
|
||||
|
@ -322,7 +322,7 @@ class Exchange:
|
|||
return int(self._ft_has.get('ohlcv_candle_limit_per_timeframe', {}).get(
|
||||
timeframe, self._ft_has.get('ohlcv_candle_limit')))
|
||||
|
||||
def get_markets(self, base_currencies: List[str] = None, quote_currencies: List[str] = None,
|
||||
def get_markets(self, base_currencies: List[str] = [], quote_currencies: List[str] = [],
|
||||
spot_only: bool = False, margin_only: bool = False, futures_only: bool = False,
|
||||
tradable_only: bool = True,
|
||||
active_only: bool = False) -> Dict[str, Any]:
|
||||
|
@ -953,6 +953,12 @@ class Exchange:
|
|||
order = self.check_dry_limit_order_filled(order)
|
||||
return order
|
||||
except KeyError as e:
|
||||
from freqtrade.persistence import Order
|
||||
order = Order.order_by_id(order_id)
|
||||
if order:
|
||||
ccxt_order = order.to_ccxt_object()
|
||||
self._dry_run_open_orders[order_id] = ccxt_order
|
||||
return ccxt_order
|
||||
# Gracefully handle errors with dry-run orders.
|
||||
raise InvalidOrderException(
|
||||
f'Tried to get an invalid dry-run-order (id: {order_id}). Message: {e}') from e
|
||||
|
@ -1158,7 +1164,7 @@ class Exchange:
|
|||
raise OperationalException(e) from e
|
||||
|
||||
@retrier(retries=API_FETCH_ORDER_RETRY_COUNT)
|
||||
def fetch_order(self, order_id: str, pair: str, params={}) -> Dict:
|
||||
def fetch_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
|
||||
if self._config['dry_run']:
|
||||
return self.fetch_dry_run_order(order_id)
|
||||
try:
|
||||
|
@ -1180,8 +1186,8 @@ class Exchange:
|
|||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
# Assign method to fetch_stoploss_order to allow easy overriding in other classes
|
||||
fetch_stoploss_order = fetch_order
|
||||
def fetch_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
|
||||
return self.fetch_order(order_id, pair, params)
|
||||
|
||||
def fetch_order_or_stoploss_order(self, order_id: str, pair: str,
|
||||
stoploss_order: bool = False) -> Dict:
|
||||
|
@ -1206,7 +1212,7 @@ class Exchange:
|
|||
and order.get('filled') == 0.0)
|
||||
|
||||
@retrier
|
||||
def cancel_order(self, order_id: str, pair: str, params={}) -> Dict:
|
||||
def cancel_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
|
||||
if self._config['dry_run']:
|
||||
try:
|
||||
order = self.fetch_dry_run_order(order_id)
|
||||
|
@ -1232,8 +1238,8 @@ class Exchange:
|
|||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
# Assign method to cancel_stoploss_order to allow easy overriding in other classes
|
||||
cancel_stoploss_order = cancel_order
|
||||
def cancel_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
|
||||
return self.cancel_order(order_id, pair, params)
|
||||
|
||||
def is_cancel_order_result_suitable(self, corder) -> bool:
|
||||
if not isinstance(corder, dict):
|
||||
|
@ -1345,7 +1351,7 @@ class Exchange:
|
|||
raise OperationalException(e) from e
|
||||
|
||||
@retrier
|
||||
def fetch_bids_asks(self, symbols: List[str] = None, cached: bool = False) -> Dict:
|
||||
def fetch_bids_asks(self, symbols: Optional[List[str]] = None, cached: bool = False) -> Dict:
|
||||
"""
|
||||
:param cached: Allow cached result
|
||||
:return: fetch_tickers result
|
||||
|
@ -1373,7 +1379,7 @@ class Exchange:
|
|||
raise OperationalException(e) from e
|
||||
|
||||
@retrier
|
||||
def get_tickers(self, symbols: List[str] = None, cached: bool = False) -> Dict:
|
||||
def get_tickers(self, symbols: Optional[List[str]] = None, cached: bool = False) -> Dict:
|
||||
"""
|
||||
:param cached: Allow cached result
|
||||
:return: fetch_tickers result
|
||||
|
@ -1712,7 +1718,7 @@ class Exchange:
|
|||
async def _async_get_historic_ohlcv(self, pair: str, timeframe: str,
|
||||
since_ms: int, candle_type: CandleType,
|
||||
is_new_pair: bool = False, raise_: bool = False,
|
||||
until_ms: int = None
|
||||
until_ms: Optional[int] = None
|
||||
) -> Tuple[str, str, str, List]:
|
||||
"""
|
||||
Download historic ohlcv
|
||||
|
@ -1773,7 +1779,7 @@ class Exchange:
|
|||
|
||||
def refresh_latest_ohlcv(self, pair_list: ListPairsWithTimeframes, *,
|
||||
since_ms: Optional[int] = None, cache: bool = True,
|
||||
drop_incomplete: bool = None
|
||||
drop_incomplete: Optional[bool] = None
|
||||
) -> Dict[PairWithTimeframe, DataFrame]:
|
||||
"""
|
||||
Refresh in-memory OHLCV asynchronously and set `_klines` with the result
|
||||
|
@ -2413,14 +2419,35 @@ class Exchange:
|
|||
)
|
||||
|
||||
@staticmethod
|
||||
def combine_funding_and_mark(funding_rates: DataFrame, mark_rates: DataFrame) -> DataFrame:
|
||||
def combine_funding_and_mark(funding_rates: DataFrame, mark_rates: DataFrame,
|
||||
futures_funding_rate: Optional[int] = None) -> DataFrame:
|
||||
"""
|
||||
Combine funding-rates and mark-rates dataframes
|
||||
:param funding_rates: Dataframe containing Funding rates (Type FUNDING_RATE)
|
||||
:param mark_rates: Dataframe containing Mark rates (Type mark_ohlcv_price)
|
||||
:param futures_funding_rate: Fake funding rate to use if funding_rates are not available
|
||||
"""
|
||||
if futures_funding_rate is None:
|
||||
return mark_rates.merge(
|
||||
funding_rates, on='date', how="inner", suffixes=["_mark", "_fund"])
|
||||
else:
|
||||
if len(funding_rates) == 0:
|
||||
# No funding rate candles - full fillup with fallback variable
|
||||
mark_rates['open_fund'] = futures_funding_rate
|
||||
return mark_rates.rename(
|
||||
columns={'open': 'open_mark',
|
||||
'close': 'close_mark',
|
||||
'high': 'high_mark',
|
||||
'low': 'low_mark',
|
||||
'volume': 'volume_mark'})
|
||||
|
||||
return funding_rates.merge(mark_rates, on='date', how="inner", suffixes=["_fund", "_mark"])
|
||||
else:
|
||||
# Fill up missing funding_rate candles with fallback value
|
||||
combined = mark_rates.merge(
|
||||
funding_rates, on='date', how="outer", suffixes=["_mark", "_fund"]
|
||||
)
|
||||
combined['open_fund'] = combined['open_fund'].fillna(futures_funding_rate)
|
||||
return combined
|
||||
|
||||
def calculate_funding_fees(
|
||||
self,
|
||||
|
|
|
@ -104,7 +104,7 @@ class Ftx(Exchange):
|
|||
raise OperationalException(e) from e
|
||||
|
||||
@retrier(retries=API_FETCH_ORDER_RETRY_COUNT)
|
||||
def fetch_stoploss_order(self, order_id: str, pair: str) -> Dict:
|
||||
def fetch_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
|
||||
if self._config['dry_run']:
|
||||
return self.fetch_dry_run_order(order_id)
|
||||
|
||||
|
@ -145,7 +145,7 @@ class Ftx(Exchange):
|
|||
raise OperationalException(e) from e
|
||||
|
||||
@retrier
|
||||
def cancel_stoploss_order(self, order_id: str, pair: str) -> Dict:
|
||||
def cancel_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
|
||||
if self._config['dry_run']:
|
||||
return {}
|
||||
try:
|
||||
|
|
|
@ -71,14 +71,14 @@ class Gateio(Exchange):
|
|||
}
|
||||
return trades
|
||||
|
||||
def fetch_stoploss_order(self, order_id: str, pair: str, params={}) -> Dict:
|
||||
def fetch_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
|
||||
return self.fetch_order(
|
||||
order_id=order_id,
|
||||
pair=pair,
|
||||
params={'stop': True}
|
||||
)
|
||||
|
||||
def cancel_stoploss_order(self, order_id: str, pair: str, params={}) -> Dict:
|
||||
def cancel_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
|
||||
return self.cancel_order(
|
||||
order_id=order_id,
|
||||
pair=pair,
|
||||
|
|
|
@ -45,7 +45,7 @@ class Kraken(Exchange):
|
|||
return (parent_check and
|
||||
market.get('darkpool', False) is False)
|
||||
|
||||
def get_tickers(self, symbols: List[str] = None, cached: bool = False) -> Dict:
|
||||
def get_tickers(self, symbols: Optional[List[str]] = None, cached: bool = False) -> Dict:
|
||||
# Only fetch tickers for current stake currency
|
||||
# Otherwise the request for kraken becomes too large.
|
||||
symbols = list(self.get_markets(quote_currencies=[self._config['stake_currency']]))
|
||||
|
|
|
@ -67,7 +67,7 @@ class FreqtradeBot(LoggingMixin):
|
|||
|
||||
self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config)
|
||||
|
||||
init_db(self.config.get('db_url', None), clean_open_orders=self.config['dry_run'])
|
||||
init_db(self.config.get('db_url', None))
|
||||
|
||||
self.wallets = Wallets(self.config, self.exchange)
|
||||
|
||||
|
@ -123,7 +123,7 @@ class FreqtradeBot(LoggingMixin):
|
|||
self._schedule.every().day.at(t).do(update)
|
||||
self.last_process = datetime(1970, 1, 1, tzinfo=timezone.utc)
|
||||
|
||||
self.strategy.bot_start()
|
||||
self.strategy.ft_bot_start()
|
||||
|
||||
def notify_status(self, msg: str) -> None:
|
||||
"""
|
||||
|
@ -299,7 +299,8 @@ class FreqtradeBot(LoggingMixin):
|
|||
fo = self.exchange.fetch_order_or_stoploss_order(order.order_id, order.ft_pair,
|
||||
order.ft_order_side == 'stoploss')
|
||||
|
||||
self.update_trade_state(order.trade, order.order_id, fo)
|
||||
self.update_trade_state(order.trade, order.order_id, fo,
|
||||
stoploss_order=(order.ft_order_side == 'stoploss'))
|
||||
|
||||
except ExchangeError as e:
|
||||
|
||||
|
@ -1018,7 +1019,7 @@ class FreqtradeBot(LoggingMixin):
|
|||
# Lock pair for one candle to prevent immediate rebuys
|
||||
self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc),
|
||||
reason='Auto lock')
|
||||
self._notify_exit(trade, "stoploss")
|
||||
self._notify_exit(trade, "stoploss", True)
|
||||
return True
|
||||
|
||||
if trade.open_order_id or not trade.is_open:
|
||||
|
@ -1105,7 +1106,7 @@ class FreqtradeBot(LoggingMixin):
|
|||
"""
|
||||
Check and execute trade exit
|
||||
"""
|
||||
should_exit: ExitCheckTuple = self.strategy.should_exit(
|
||||
exits: List[ExitCheckTuple] = self.strategy.should_exit(
|
||||
trade,
|
||||
exit_rate,
|
||||
datetime.now(timezone.utc),
|
||||
|
@ -1113,12 +1114,13 @@ class FreqtradeBot(LoggingMixin):
|
|||
exit_=exit_,
|
||||
force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0
|
||||
)
|
||||
|
||||
if should_exit.exit_flag:
|
||||
logger.info(f'Exit for {trade.pair} detected. Reason: {should_exit.exit_type}'
|
||||
f'Tag: {exit_tag if exit_tag is not None else "None"}')
|
||||
self.execute_trade_exit(trade, exit_rate, should_exit, exit_tag=exit_tag)
|
||||
return True
|
||||
for should_exit in exits:
|
||||
if should_exit.exit_flag:
|
||||
logger.info(f'Exit for {trade.pair} detected. Reason: {should_exit.exit_type}'
|
||||
f'{f" Tag: {exit_tag}" if exit_tag is not None else ""}')
|
||||
exited = self.execute_trade_exit(trade, exit_rate, should_exit, exit_tag=exit_tag)
|
||||
if exited:
|
||||
return True
|
||||
return False
|
||||
|
||||
def manage_open_orders(self) -> None:
|
||||
|
@ -1405,7 +1407,7 @@ class FreqtradeBot(LoggingMixin):
|
|||
:param trade: Trade instance
|
||||
:param limit: limit rate for the sell order
|
||||
:param exit_check: CheckTuple with signal and reason
|
||||
:return: True if it succeeds (supported) False (not supported)
|
||||
:return: True if it succeeds False
|
||||
"""
|
||||
trade.funding_fees = self.exchange.get_funding_fees(
|
||||
pair=trade.pair,
|
||||
|
@ -1452,7 +1454,7 @@ class FreqtradeBot(LoggingMixin):
|
|||
time_in_force=time_in_force, exit_reason=exit_reason,
|
||||
sell_reason=exit_reason, # sellreason -> compatibility
|
||||
current_time=datetime.now(timezone.utc)):
|
||||
logger.info(f"User requested abortion of exiting {trade.pair}")
|
||||
logger.info(f"User requested abortion of {trade.pair} exit.")
|
||||
return False
|
||||
|
||||
try:
|
||||
|
@ -1663,7 +1665,7 @@ class FreqtradeBot(LoggingMixin):
|
|||
if send_msg and not stoploss_order and not trade.open_order_id:
|
||||
self._notify_exit(trade, '', True)
|
||||
self.handle_protections(trade.pair, trade.trade_direction)
|
||||
elif send_msg and not trade.open_order_id:
|
||||
elif send_msg and not trade.open_order_id and not stoploss_order:
|
||||
# Enter fill
|
||||
self._notify_enter(trade, order, fill=True)
|
||||
|
||||
|
|
|
@ -188,7 +188,10 @@ class Backtesting:
|
|||
# since a "perfect" stoploss-exit is assumed anyway
|
||||
# And the regular "stoploss" function would not apply to that case
|
||||
self.strategy.order_types['stoploss_on_exchange'] = False
|
||||
self.strategy.bot_start()
|
||||
if self.dataprovider.runmode == RunMode.BACKTEST:
|
||||
# in hyperopt mode - don't re-init params
|
||||
self.strategy.ft_load_hyper_params(False)
|
||||
self.strategy.ft_bot_start()
|
||||
|
||||
def _load_protections(self, strategy: IStrategy):
|
||||
if self.config.get('enable_protections', False):
|
||||
|
@ -276,8 +279,12 @@ class Backtesting:
|
|||
if pair not in self.exchange._leverage_tiers:
|
||||
unavailable_pairs.append(pair)
|
||||
continue
|
||||
self.futures_data[pair] = funding_rates_dict[pair].merge(
|
||||
mark_rates_dict[pair], on='date', how="inner", suffixes=["_fund", "_mark"])
|
||||
|
||||
self.futures_data[pair] = self.exchange.combine_funding_and_mark(
|
||||
funding_rates=funding_rates_dict[pair],
|
||||
mark_rates=mark_rates_dict[pair],
|
||||
futures_funding_rate=self.config.get('futures_funding_rate', None),
|
||||
)
|
||||
|
||||
if unavailable_pairs:
|
||||
raise OperationalException(
|
||||
|
@ -499,7 +506,8 @@ class Backtesting:
|
|||
stake_available = self.wallets.get_available_stake_amount()
|
||||
stake_amount = strategy_safe_wrapper(self.strategy.adjust_trade_position,
|
||||
default_retval=None)(
|
||||
trade=trade, current_time=row[DATE_IDX].to_pydatetime(), current_rate=row[OPEN_IDX],
|
||||
trade=trade, # type: ignore[arg-type]
|
||||
current_time=row[DATE_IDX].to_pydatetime(), current_rate=row[OPEN_IDX],
|
||||
current_profit=current_profit, min_stake=min_stake,
|
||||
max_stake=min(max_stake, stake_available))
|
||||
|
||||
|
@ -530,15 +538,23 @@ class Backtesting:
|
|||
if check_adjust_entry:
|
||||
trade = self._get_adjust_trade_entry_for_candle(trade, row)
|
||||
|
||||
exit_candle_time: datetime = row[DATE_IDX].to_pydatetime()
|
||||
enter = row[SHORT_IDX] if trade.is_short else row[LONG_IDX]
|
||||
exit_sig = row[ESHORT_IDX] if trade.is_short else row[ELONG_IDX]
|
||||
exit_ = self.strategy.should_exit(
|
||||
trade, row[OPEN_IDX], exit_candle_time, # type: ignore
|
||||
exits = self.strategy.should_exit(
|
||||
trade, row[OPEN_IDX], row[DATE_IDX].to_pydatetime(), # type: ignore
|
||||
enter=enter, exit_=exit_sig,
|
||||
low=row[LOW_IDX], high=row[HIGH_IDX]
|
||||
)
|
||||
for exit_ in exits:
|
||||
t = self._get_exit_for_signal(trade, row, exit_)
|
||||
if t:
|
||||
return t
|
||||
return None
|
||||
|
||||
def _get_exit_for_signal(self, trade: LocalTrade, row: Tuple,
|
||||
exit_: ExitCheckTuple) -> Optional[LocalTrade]:
|
||||
|
||||
exit_candle_time: datetime = row[DATE_IDX].to_pydatetime()
|
||||
if exit_.exit_flag:
|
||||
trade.close_date = exit_candle_time
|
||||
exit_reason = exit_.exit_reason
|
||||
|
@ -565,7 +581,8 @@ class Backtesting:
|
|||
if order_type == 'limit':
|
||||
close_rate = strategy_safe_wrapper(self.strategy.custom_exit_price,
|
||||
default_retval=close_rate)(
|
||||
pair=trade.pair, trade=trade,
|
||||
pair=trade.pair,
|
||||
trade=trade, # type: ignore[arg-type]
|
||||
current_time=exit_candle_time,
|
||||
proposed_rate=close_rate, current_profit=current_profit,
|
||||
exit_tag=exit_reason)
|
||||
|
@ -579,7 +596,10 @@ class Backtesting:
|
|||
time_in_force = self.strategy.order_time_in_force['exit']
|
||||
|
||||
if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)(
|
||||
pair=trade.pair, trade=trade, order_type='limit', amount=trade.amount,
|
||||
pair=trade.pair,
|
||||
trade=trade, # type: ignore[arg-type]
|
||||
order_type='limit',
|
||||
amount=trade.amount,
|
||||
rate=close_rate,
|
||||
time_in_force=time_in_force,
|
||||
sell_reason=exit_reason, # deprecated
|
||||
|
@ -655,7 +675,7 @@ class Backtesting:
|
|||
return self._get_exit_trade_entry_for_candle(trade, row)
|
||||
|
||||
def get_valid_price_and_stake(
|
||||
self, pair: str, row: Tuple, propose_rate: float, stake_amount: Optional[float],
|
||||
self, pair: str, row: Tuple, propose_rate: float, stake_amount: float,
|
||||
direction: LongShort, current_time: datetime, entry_tag: Optional[str],
|
||||
trade: Optional[LocalTrade], order_type: str
|
||||
) -> Tuple[float, float, float, float]:
|
||||
|
@ -729,8 +749,9 @@ class Backtesting:
|
|||
order_type = self.strategy.order_types['entry']
|
||||
pos_adjust = trade is not None and requested_rate is None
|
||||
|
||||
stake_amount_ = stake_amount or (trade.stake_amount if trade else 0.0)
|
||||
propose_rate, stake_amount, leverage, min_stake_amount = self.get_valid_price_and_stake(
|
||||
pair, row, row[OPEN_IDX], stake_amount, direction, current_time, entry_tag, trade,
|
||||
pair, row, row[OPEN_IDX], stake_amount_, direction, current_time, entry_tag, trade,
|
||||
order_type
|
||||
)
|
||||
|
||||
|
@ -900,7 +921,9 @@ class Backtesting:
|
|||
Check if current analyzed order has to be canceled.
|
||||
Returns True if the trade should be Deleted (initial order was canceled).
|
||||
"""
|
||||
timedout = self.strategy.ft_check_timed_out(trade, order, current_time)
|
||||
timedout = self.strategy.ft_check_timed_out(
|
||||
trade, # type: ignore[arg-type]
|
||||
order, current_time)
|
||||
if timedout:
|
||||
if order.side == trade.entry_side:
|
||||
self.timedout_entry_orders += 1
|
||||
|
@ -929,7 +952,8 @@ class Backtesting:
|
|||
if order.side == trade.entry_side and current_time > order.order_date_utc:
|
||||
requested_rate = strategy_safe_wrapper(self.strategy.adjust_entry_price,
|
||||
default_retval=order.price)(
|
||||
trade=trade, order=order, pair=trade.pair, current_time=current_time,
|
||||
trade=trade, # type: ignore[arg-type]
|
||||
order=order, pair=trade.pair, current_time=current_time,
|
||||
proposed_rate=row[OPEN_IDX], current_order_rate=order.price,
|
||||
entry_tag=trade.enter_tag, side=trade.trade_direction
|
||||
) # default value is current order price
|
||||
|
|
|
@ -44,7 +44,7 @@ class EdgeCli:
|
|||
|
||||
self.edge._timerange = TimeRange.parse_timerange(None if self.config.get(
|
||||
'timerange') is None else str(self.config.get('timerange')))
|
||||
self.strategy.bot_start()
|
||||
self.strategy.ft_bot_start()
|
||||
|
||||
def start(self) -> None:
|
||||
result = self.edge.calculate(self.config['exchange']['pair_whitelist'])
|
||||
|
|
|
@ -27,8 +27,7 @@ from freqtrade.misc import deep_merge_dicts, file_dump_json, plural
|
|||
from freqtrade.optimize.backtesting import Backtesting
|
||||
# Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules
|
||||
from freqtrade.optimize.hyperopt_auto import HyperOptAuto
|
||||
from freqtrade.optimize.hyperopt_interface import IHyperOpt # noqa: F401
|
||||
from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F401
|
||||
from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss
|
||||
from freqtrade.optimize.hyperopt_tools import HyperoptTools, hyperopt_serializer
|
||||
from freqtrade.optimize.optimize_reports import generate_strategy_stats
|
||||
from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver
|
||||
|
@ -62,7 +61,6 @@ class Hyperopt:
|
|||
hyperopt = Hyperopt(config)
|
||||
hyperopt.start()
|
||||
"""
|
||||
custom_hyperopt: IHyperOpt
|
||||
|
||||
def __init__(self, config: Dict[str, Any]) -> None:
|
||||
self.buy_space: List[Dimension] = []
|
||||
|
@ -77,6 +75,7 @@ class Hyperopt:
|
|||
|
||||
self.backtesting = Backtesting(self.config)
|
||||
self.pairlist = self.backtesting.pairlists.whitelist
|
||||
self.custom_hyperopt: HyperOptAuto
|
||||
|
||||
if not self.config.get('hyperopt'):
|
||||
self.custom_hyperopt = HyperOptAuto(self.config)
|
||||
|
@ -88,7 +87,8 @@ class Hyperopt:
|
|||
self.backtesting._set_strategy(self.backtesting.strategylist[0])
|
||||
self.custom_hyperopt.strategy = self.backtesting.strategy
|
||||
|
||||
self.custom_hyperoptloss = HyperOptLossResolver.load_hyperoptloss(self.config)
|
||||
self.custom_hyperoptloss: IHyperOptLoss = HyperOptLossResolver.load_hyperoptloss(
|
||||
self.config)
|
||||
self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function
|
||||
time_now = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
||||
strategy = str(self.config['strategy'])
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# flake8: noqa: F401
|
||||
|
||||
from freqtrade.persistence.keyvalue_middleware import KeyValues
|
||||
from freqtrade.persistence.models import clean_dry_run_db, cleanup_db, init_db
|
||||
from freqtrade.persistence.models import cleanup_db, init_db
|
||||
from freqtrade.persistence.pairlock_middleware import PairLocks
|
||||
from freqtrade.persistence.trade_model import LocalTrade, Order, Trade
|
||||
|
|
|
@ -22,14 +22,12 @@ logger = logging.getLogger(__name__)
|
|||
_SQL_DOCS_URL = 'http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls'
|
||||
|
||||
|
||||
def init_db(db_url: str, clean_open_orders: bool = False) -> None:
|
||||
def init_db(db_url: str) -> None:
|
||||
"""
|
||||
Initializes this module with the given config,
|
||||
registers all known command handlers
|
||||
and starts polling for message updates
|
||||
:param db_url: Database to use
|
||||
:param clean_open_orders: Remove open orders from the database.
|
||||
Useful for dry-run or if all orders have been reset on the exchange.
|
||||
:return: None
|
||||
"""
|
||||
kwargs = {}
|
||||
|
@ -66,10 +64,6 @@ def init_db(db_url: str, clean_open_orders: bool = False) -> None:
|
|||
_DECL_BASE.metadata.create_all(engine)
|
||||
check_migrate(engine, decl_base=_DECL_BASE, previous_tables=previous_tables)
|
||||
|
||||
# Clean dry_run DB if the db is not in-memory
|
||||
if clean_open_orders and db_url != 'sqlite://':
|
||||
clean_dry_run_db()
|
||||
|
||||
|
||||
def cleanup_db() -> None:
|
||||
"""
|
||||
|
@ -77,15 +71,3 @@ def cleanup_db() -> None:
|
|||
:return: None
|
||||
"""
|
||||
Trade.commit()
|
||||
|
||||
|
||||
def clean_dry_run_db() -> None:
|
||||
"""
|
||||
Remove open_order_id from a Dry_run DB
|
||||
:return: None
|
||||
"""
|
||||
for trade in Trade.query.filter(Trade.open_order_id.isnot(None)).all():
|
||||
# Check we are updating only a dry_run order not a prod one
|
||||
if 'dry_run' in trade.open_order_id:
|
||||
trade.open_order_id = None
|
||||
Trade.commit()
|
||||
|
|
|
@ -120,6 +120,25 @@ class Order(_DECL_BASE):
|
|||
self.order_filled_date = datetime.now(timezone.utc)
|
||||
self.order_update_date = datetime.now(timezone.utc)
|
||||
|
||||
def to_ccxt_object(self) -> Dict[str, Any]:
|
||||
return {
|
||||
'id': self.order_id,
|
||||
'symbol': self.ft_pair,
|
||||
'price': self.price,
|
||||
'average': self.average,
|
||||
'amount': self.amount,
|
||||
'cost': self.cost,
|
||||
'type': self.order_type,
|
||||
'side': self.ft_order_side,
|
||||
'filled': self.filled,
|
||||
'remaining': self.remaining,
|
||||
'datetime': self.order_date_utc.strftime('%Y-%m-%dT%H:%M:%S.%f'),
|
||||
'timestamp': int(self.order_date_utc.timestamp() * 1000),
|
||||
'status': self.status,
|
||||
'fee': None,
|
||||
'info': {},
|
||||
}
|
||||
|
||||
def to_json(self, entry_side: str) -> Dict[str, Any]:
|
||||
return {
|
||||
'pair': self.ft_pair,
|
||||
|
@ -192,6 +211,14 @@ class Order(_DECL_BASE):
|
|||
"""
|
||||
return Order.query.filter(Order.ft_is_open.is_(True)).all()
|
||||
|
||||
@staticmethod
|
||||
def order_by_id(order_id: str) -> Optional['Order']:
|
||||
"""
|
||||
Retrieve order based on order_id
|
||||
:return: Order or None
|
||||
"""
|
||||
return Order.query.filter(Order.order_id == order_id).first()
|
||||
|
||||
|
||||
class LocalTrade():
|
||||
"""
|
||||
|
@ -844,8 +871,8 @@ class LocalTrade():
|
|||
return o
|
||||
return None
|
||||
|
||||
def select_order(
|
||||
self, order_side: str = None, is_open: Optional[bool] = None) -> Optional[Order]:
|
||||
def select_order(self, order_side: Optional[str] = None,
|
||||
is_open: Optional[bool] = None) -> Optional[Order]:
|
||||
"""
|
||||
Finds latest order for this orderside and status
|
||||
:param order_side: ft_order_side of the order (either 'buy', 'sell' or 'stoploss')
|
||||
|
|
|
@ -633,7 +633,7 @@ def load_and_plot_trades(config: Dict[str, Any]):
|
|||
|
||||
exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config)
|
||||
IStrategy.dp = DataProvider(config, exchange)
|
||||
strategy.bot_start()
|
||||
strategy.ft_bot_start()
|
||||
strategy.bot_loop_start()
|
||||
plot_elements = init_plotscript(config, list(exchange.markets), strategy.startup_candle_count)
|
||||
timerange = plot_elements['timerange']
|
||||
|
|
|
@ -50,7 +50,7 @@ class SpreadFilter(IPairList):
|
|||
:param ticker: ticker dict as returned from ccxt.fetch_tickers()
|
||||
:return: True if the pair can stay, false if it should be removed
|
||||
"""
|
||||
if 'bid' in ticker and 'ask' in ticker and ticker['ask']:
|
||||
if 'bid' in ticker and 'ask' in ticker and ticker['ask'] and ticker['bid']:
|
||||
spread = 1 - ticker['bid'] / ticker['ask']
|
||||
if spread > self._max_spread_ratio:
|
||||
self.log_once(f"Removed {pair} from whitelist, because spread "
|
||||
|
|
|
@ -47,26 +47,7 @@ class StrategyResolver(IResolver):
|
|||
strategy: IStrategy = StrategyResolver._load_strategy(
|
||||
strategy_name, config=config,
|
||||
extra_dir=config.get('strategy_path'))
|
||||
|
||||
if strategy._ft_params_from_file:
|
||||
# Set parameters from Hyperopt results file
|
||||
params = strategy._ft_params_from_file
|
||||
strategy.minimal_roi = params.get('roi', getattr(strategy, 'minimal_roi', {}))
|
||||
|
||||
strategy.stoploss = params.get('stoploss', {}).get(
|
||||
'stoploss', getattr(strategy, 'stoploss', -0.1))
|
||||
trailing = params.get('trailing', {})
|
||||
strategy.trailing_stop = trailing.get(
|
||||
'trailing_stop', getattr(strategy, 'trailing_stop', False))
|
||||
strategy.trailing_stop_positive = trailing.get(
|
||||
'trailing_stop_positive', getattr(strategy, 'trailing_stop_positive', None))
|
||||
strategy.trailing_stop_positive_offset = trailing.get(
|
||||
'trailing_stop_positive_offset',
|
||||
getattr(strategy, 'trailing_stop_positive_offset', 0))
|
||||
strategy.trailing_only_offset_is_reached = trailing.get(
|
||||
'trailing_only_offset_is_reached',
|
||||
getattr(strategy, 'trailing_only_offset_is_reached', 0.0))
|
||||
|
||||
strategy.ft_load_params_from_file()
|
||||
# Set attributes
|
||||
# Check if we need to override configuration
|
||||
# (Attribute name, default, subkey)
|
||||
|
|
|
@ -256,6 +256,7 @@ class TradeSchema(BaseModel):
|
|||
|
||||
leverage: Optional[float]
|
||||
interest_rate: Optional[float]
|
||||
liquidation_price: Optional[float]
|
||||
funding_fees: Optional[float]
|
||||
trading_mode: Optional[TradingMode]
|
||||
|
||||
|
|
|
@ -785,7 +785,7 @@ class Telegram(RPCHandler):
|
|||
headers=['Exit Reason', 'Exits', 'Wins', 'Losses']
|
||||
)
|
||||
if len(exit_reasons_tabulate) > 25:
|
||||
self._send_msg(exit_reasons_msg, ParseMode.MARKDOWN)
|
||||
self._send_msg(f"```\n{exit_reasons_msg}```", ParseMode.MARKDOWN)
|
||||
exit_reasons_msg = ''
|
||||
|
||||
durations = stats['durations']
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
# flake8: noqa: F401
|
||||
from freqtrade.exchange import (timeframe_to_minutes, timeframe_to_msecs, timeframe_to_next_date,
|
||||
timeframe_to_prev_date, timeframe_to_seconds)
|
||||
from freqtrade.strategy.hyper import (BooleanParameter, CategoricalParameter, DecimalParameter,
|
||||
IntParameter, RealParameter)
|
||||
from freqtrade.strategy.informative_decorator import informative
|
||||
from freqtrade.strategy.interface import IStrategy
|
||||
from freqtrade.strategy.parameters import (BooleanParameter, CategoricalParameter, DecimalParameter,
|
||||
IntParameter, RealParameter)
|
||||
from freqtrade.strategy.strategy_helper import (merge_informative_pair, stoploss_from_absolute,
|
||||
stoploss_from_open)
|
||||
|
|
|
@ -3,295 +3,19 @@ IHyperStrategy interface, hyperoptable Parameter class.
|
|||
This module defines a base class for auto-hyperoptable strategies.
|
||||
"""
|
||||
import logging
|
||||
from abc import ABC, abstractmethod
|
||||
from contextlib import suppress
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Iterator, List, Optional, Sequence, Tuple, Union
|
||||
|
||||
from freqtrade.misc import deep_merge_dicts, json_load
|
||||
from freqtrade.optimize.hyperopt_tools import HyperoptTools
|
||||
|
||||
|
||||
with suppress(ImportError):
|
||||
from skopt.space import Integer, Real, Categorical
|
||||
from freqtrade.optimize.space import SKDecimal
|
||||
from typing import Any, Dict, Iterator, List, Tuple
|
||||
|
||||
from freqtrade.enums import RunMode
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.misc import deep_merge_dicts, json_load
|
||||
from freqtrade.optimize.hyperopt_tools import HyperoptTools
|
||||
from freqtrade.strategy.parameters import BaseParameter
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BaseParameter(ABC):
|
||||
"""
|
||||
Defines a parameter that can be optimized by hyperopt.
|
||||
"""
|
||||
category: Optional[str]
|
||||
default: Any
|
||||
value: Any
|
||||
in_space: bool = False
|
||||
name: str
|
||||
|
||||
def __init__(self, *, default: Any, space: Optional[str] = None,
|
||||
optimize: bool = True, load: bool = True, **kwargs):
|
||||
"""
|
||||
Initialize hyperopt-optimizable parameter.
|
||||
:param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if
|
||||
parameter field
|
||||
name is prefixed with 'buy_' or 'sell_'.
|
||||
:param optimize: Include parameter in hyperopt optimizations.
|
||||
:param load: Load parameter value from {space}_params.
|
||||
:param kwargs: Extra parameters to skopt.space.(Integer|Real|Categorical).
|
||||
"""
|
||||
if 'name' in kwargs:
|
||||
raise OperationalException(
|
||||
'Name is determined by parameter field name and can not be specified manually.')
|
||||
self.category = space
|
||||
self._space_params = kwargs
|
||||
self.value = default
|
||||
self.optimize = optimize
|
||||
self.load = load
|
||||
|
||||
def __repr__(self):
|
||||
return f'{self.__class__.__name__}({self.value})'
|
||||
|
||||
@abstractmethod
|
||||
def get_space(self, name: str) -> Union['Integer', 'Real', 'SKDecimal', 'Categorical']:
|
||||
"""
|
||||
Get-space - will be used by Hyperopt to get the hyperopt Space
|
||||
"""
|
||||
|
||||
|
||||
class NumericParameter(BaseParameter):
|
||||
""" Internal parameter used for Numeric purposes """
|
||||
float_or_int = Union[int, float]
|
||||
default: float_or_int
|
||||
value: float_or_int
|
||||
|
||||
def __init__(self, low: Union[float_or_int, Sequence[float_or_int]],
|
||||
high: Optional[float_or_int] = None, *, default: float_or_int,
|
||||
space: Optional[str] = None, optimize: bool = True, load: bool = True, **kwargs):
|
||||
"""
|
||||
Initialize hyperopt-optimizable numeric parameter.
|
||||
Cannot be instantiated, but provides the validation for other numeric parameters
|
||||
:param low: Lower end (inclusive) of optimization space or [low, high].
|
||||
:param high: Upper end (inclusive) of optimization space.
|
||||
Must be none of entire range is passed first parameter.
|
||||
:param default: A default value.
|
||||
:param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if
|
||||
parameter fieldname is prefixed with 'buy_' or 'sell_'.
|
||||
:param optimize: Include parameter in hyperopt optimizations.
|
||||
:param load: Load parameter value from {space}_params.
|
||||
:param kwargs: Extra parameters to skopt.space.*.
|
||||
"""
|
||||
if high is not None and isinstance(low, Sequence):
|
||||
raise OperationalException(f'{self.__class__.__name__} space invalid.')
|
||||
if high is None or isinstance(low, Sequence):
|
||||
if not isinstance(low, Sequence) or len(low) != 2:
|
||||
raise OperationalException(f'{self.__class__.__name__} space must be [low, high]')
|
||||
self.low, self.high = low
|
||||
else:
|
||||
self.low = low
|
||||
self.high = high
|
||||
|
||||
super().__init__(default=default, space=space, optimize=optimize,
|
||||
load=load, **kwargs)
|
||||
|
||||
|
||||
class IntParameter(NumericParameter):
|
||||
default: int
|
||||
value: int
|
||||
|
||||
def __init__(self, low: Union[int, Sequence[int]], high: Optional[int] = None, *, default: int,
|
||||
space: Optional[str] = None, optimize: bool = True, load: bool = True, **kwargs):
|
||||
"""
|
||||
Initialize hyperopt-optimizable integer parameter.
|
||||
:param low: Lower end (inclusive) of optimization space or [low, high].
|
||||
:param high: Upper end (inclusive) of optimization space.
|
||||
Must be none of entire range is passed first parameter.
|
||||
:param default: A default value.
|
||||
:param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if
|
||||
parameter fieldname is prefixed with 'buy_' or 'sell_'.
|
||||
:param optimize: Include parameter in hyperopt optimizations.
|
||||
:param load: Load parameter value from {space}_params.
|
||||
:param kwargs: Extra parameters to skopt.space.Integer.
|
||||
"""
|
||||
|
||||
super().__init__(low=low, high=high, default=default, space=space, optimize=optimize,
|
||||
load=load, **kwargs)
|
||||
|
||||
def get_space(self, name: str) -> 'Integer':
|
||||
"""
|
||||
Create skopt optimization space.
|
||||
:param name: A name of parameter field.
|
||||
"""
|
||||
return Integer(low=self.low, high=self.high, name=name, **self._space_params)
|
||||
|
||||
@property
|
||||
def range(self):
|
||||
"""
|
||||
Get each value in this space as list.
|
||||
Returns a List from low to high (inclusive) in Hyperopt mode.
|
||||
Returns a List with 1 item (`value`) in "non-hyperopt" mode, to avoid
|
||||
calculating 100ds of indicators.
|
||||
"""
|
||||
if self.in_space and self.optimize:
|
||||
# Scikit-optimize ranges are "inclusive", while python's "range" is exclusive
|
||||
return range(self.low, self.high + 1)
|
||||
else:
|
||||
return range(self.value, self.value + 1)
|
||||
|
||||
|
||||
class RealParameter(NumericParameter):
|
||||
default: float
|
||||
value: float
|
||||
|
||||
def __init__(self, low: Union[float, Sequence[float]], high: Optional[float] = None, *,
|
||||
default: float, space: Optional[str] = None, optimize: bool = True,
|
||||
load: bool = True, **kwargs):
|
||||
"""
|
||||
Initialize hyperopt-optimizable floating point parameter with unlimited precision.
|
||||
:param low: Lower end (inclusive) of optimization space or [low, high].
|
||||
:param high: Upper end (inclusive) of optimization space.
|
||||
Must be none if entire range is passed first parameter.
|
||||
:param default: A default value.
|
||||
:param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if
|
||||
parameter fieldname is prefixed with 'buy_' or 'sell_'.
|
||||
:param optimize: Include parameter in hyperopt optimizations.
|
||||
:param load: Load parameter value from {space}_params.
|
||||
:param kwargs: Extra parameters to skopt.space.Real.
|
||||
"""
|
||||
super().__init__(low=low, high=high, default=default, space=space, optimize=optimize,
|
||||
load=load, **kwargs)
|
||||
|
||||
def get_space(self, name: str) -> 'Real':
|
||||
"""
|
||||
Create skopt optimization space.
|
||||
:param name: A name of parameter field.
|
||||
"""
|
||||
return Real(low=self.low, high=self.high, name=name, **self._space_params)
|
||||
|
||||
|
||||
class DecimalParameter(NumericParameter):
|
||||
default: float
|
||||
value: float
|
||||
|
||||
def __init__(self, low: Union[float, Sequence[float]], high: Optional[float] = None, *,
|
||||
default: float, decimals: int = 3, space: Optional[str] = None,
|
||||
optimize: bool = True, load: bool = True, **kwargs):
|
||||
"""
|
||||
Initialize hyperopt-optimizable decimal parameter with a limited precision.
|
||||
:param low: Lower end (inclusive) of optimization space or [low, high].
|
||||
:param high: Upper end (inclusive) of optimization space.
|
||||
Must be none if entire range is passed first parameter.
|
||||
:param default: A default value.
|
||||
:param decimals: A number of decimals after floating point to be included in testing.
|
||||
:param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if
|
||||
parameter fieldname is prefixed with 'buy_' or 'sell_'.
|
||||
:param optimize: Include parameter in hyperopt optimizations.
|
||||
:param load: Load parameter value from {space}_params.
|
||||
:param kwargs: Extra parameters to skopt.space.Integer.
|
||||
"""
|
||||
self._decimals = decimals
|
||||
default = round(default, self._decimals)
|
||||
|
||||
super().__init__(low=low, high=high, default=default, space=space, optimize=optimize,
|
||||
load=load, **kwargs)
|
||||
|
||||
def get_space(self, name: str) -> 'SKDecimal':
|
||||
"""
|
||||
Create skopt optimization space.
|
||||
:param name: A name of parameter field.
|
||||
"""
|
||||
return SKDecimal(low=self.low, high=self.high, decimals=self._decimals, name=name,
|
||||
**self._space_params)
|
||||
|
||||
@property
|
||||
def range(self):
|
||||
"""
|
||||
Get each value in this space as list.
|
||||
Returns a List from low to high (inclusive) in Hyperopt mode.
|
||||
Returns a List with 1 item (`value`) in "non-hyperopt" mode, to avoid
|
||||
calculating 100ds of indicators.
|
||||
"""
|
||||
if self.in_space and self.optimize:
|
||||
low = int(self.low * pow(10, self._decimals))
|
||||
high = int(self.high * pow(10, self._decimals)) + 1
|
||||
return [round(n * pow(0.1, self._decimals), self._decimals) for n in range(low, high)]
|
||||
else:
|
||||
return [self.value]
|
||||
|
||||
|
||||
class CategoricalParameter(BaseParameter):
|
||||
default: Any
|
||||
value: Any
|
||||
opt_range: Sequence[Any]
|
||||
|
||||
def __init__(self, categories: Sequence[Any], *, default: Optional[Any] = None,
|
||||
space: Optional[str] = None, optimize: bool = True, load: bool = True, **kwargs):
|
||||
"""
|
||||
Initialize hyperopt-optimizable parameter.
|
||||
:param categories: Optimization space, [a, b, ...].
|
||||
:param default: A default value. If not specified, first item from specified space will be
|
||||
used.
|
||||
:param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if
|
||||
parameter field
|
||||
name is prefixed with 'buy_' or 'sell_'.
|
||||
:param optimize: Include parameter in hyperopt optimizations.
|
||||
:param load: Load parameter value from {space}_params.
|
||||
:param kwargs: Extra parameters to skopt.space.Categorical.
|
||||
"""
|
||||
if len(categories) < 2:
|
||||
raise OperationalException(
|
||||
'CategoricalParameter space must be [a, b, ...] (at least two parameters)')
|
||||
self.opt_range = categories
|
||||
super().__init__(default=default, space=space, optimize=optimize,
|
||||
load=load, **kwargs)
|
||||
|
||||
def get_space(self, name: str) -> 'Categorical':
|
||||
"""
|
||||
Create skopt optimization space.
|
||||
:param name: A name of parameter field.
|
||||
"""
|
||||
return Categorical(self.opt_range, name=name, **self._space_params)
|
||||
|
||||
@property
|
||||
def range(self):
|
||||
"""
|
||||
Get each value in this space as list.
|
||||
Returns a List of categories in Hyperopt mode.
|
||||
Returns a List with 1 item (`value`) in "non-hyperopt" mode, to avoid
|
||||
calculating 100ds of indicators.
|
||||
"""
|
||||
if self.in_space and self.optimize:
|
||||
return self.opt_range
|
||||
else:
|
||||
return [self.value]
|
||||
|
||||
|
||||
class BooleanParameter(CategoricalParameter):
|
||||
|
||||
def __init__(self, *, default: Optional[Any] = None,
|
||||
space: Optional[str] = None, optimize: bool = True, load: bool = True, **kwargs):
|
||||
"""
|
||||
Initialize hyperopt-optimizable Boolean Parameter.
|
||||
It's a shortcut to `CategoricalParameter([True, False])`.
|
||||
:param default: A default value. If not specified, first item from specified space will be
|
||||
used.
|
||||
:param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if
|
||||
parameter field
|
||||
name is prefixed with 'buy_' or 'sell_'.
|
||||
:param optimize: Include parameter in hyperopt optimizations.
|
||||
:param load: Load parameter value from {space}_params.
|
||||
:param kwargs: Extra parameters to skopt.space.Categorical.
|
||||
"""
|
||||
|
||||
categories = [True, False]
|
||||
super().__init__(categories=categories, default=default, space=space, optimize=optimize,
|
||||
load=load, **kwargs)
|
||||
|
||||
|
||||
class HyperStrategyMixin:
|
||||
"""
|
||||
A helper base class which allows HyperOptAuto class to reuse implementations of buy/sell
|
||||
|
@ -307,7 +31,12 @@ class HyperStrategyMixin:
|
|||
self.ft_sell_params: List[BaseParameter] = []
|
||||
self.ft_protection_params: List[BaseParameter] = []
|
||||
|
||||
self._load_hyper_params(config.get('runmode') == RunMode.HYPEROPT)
|
||||
params = self.load_params_from_file()
|
||||
params = params.get('params', {})
|
||||
self._ft_params_from_file = params
|
||||
|
||||
if config.get('runmode') != RunMode.BACKTEST:
|
||||
self.ft_load_hyper_params(config.get('runmode') == RunMode.HYPEROPT)
|
||||
|
||||
def enumerate_parameters(self, category: str = None) -> Iterator[Tuple[str, BaseParameter]]:
|
||||
"""
|
||||
|
@ -345,7 +74,7 @@ class HyperStrategyMixin:
|
|||
@classmethod
|
||||
def detect_all_parameters(cls) -> Dict:
|
||||
""" Detect all parameters and return them as a list"""
|
||||
params: Dict = {
|
||||
params: Dict[str, Any] = {
|
||||
'buy': list(cls.detect_parameters('buy')),
|
||||
'sell': list(cls.detect_parameters('sell')),
|
||||
'protection': list(cls.detect_parameters('protection')),
|
||||
|
@ -356,21 +85,49 @@ class HyperStrategyMixin:
|
|||
|
||||
return params
|
||||
|
||||
def _load_hyper_params(self, hyperopt: bool = False) -> None:
|
||||
def ft_load_params_from_file(self) -> None:
|
||||
"""
|
||||
Load Parameters from parameter file
|
||||
Should/must run before config values are loaded in strategy_resolver.
|
||||
"""
|
||||
if self._ft_params_from_file:
|
||||
# Set parameters from Hyperopt results file
|
||||
params = self._ft_params_from_file
|
||||
self.minimal_roi = params.get('roi', getattr(self, 'minimal_roi', {}))
|
||||
|
||||
self.stoploss = params.get('stoploss', {}).get(
|
||||
'stoploss', getattr(self, 'stoploss', -0.1))
|
||||
trailing = params.get('trailing', {})
|
||||
self.trailing_stop = trailing.get(
|
||||
'trailing_stop', getattr(self, 'trailing_stop', False))
|
||||
self.trailing_stop_positive = trailing.get(
|
||||
'trailing_stop_positive', getattr(self, 'trailing_stop_positive', None))
|
||||
self.trailing_stop_positive_offset = trailing.get(
|
||||
'trailing_stop_positive_offset',
|
||||
getattr(self, 'trailing_stop_positive_offset', 0))
|
||||
self.trailing_only_offset_is_reached = trailing.get(
|
||||
'trailing_only_offset_is_reached',
|
||||
getattr(self, 'trailing_only_offset_is_reached', 0.0))
|
||||
|
||||
def ft_load_hyper_params(self, hyperopt: bool = False) -> None:
|
||||
"""
|
||||
Load Hyperoptable parameters
|
||||
Prevalence:
|
||||
* Parameters from parameter file
|
||||
* Parameters defined in parameters objects (buy_params, sell_params, ...)
|
||||
* Parameter defaults
|
||||
"""
|
||||
params = self.load_params_from_file()
|
||||
params = params.get('params', {})
|
||||
self._ft_params_from_file = params
|
||||
buy_params = deep_merge_dicts(params.get('buy', {}), getattr(self, 'buy_params', {}))
|
||||
sell_params = deep_merge_dicts(params.get('sell', {}), getattr(self, 'sell_params', {}))
|
||||
protection_params = deep_merge_dicts(params.get('protection', {}),
|
||||
|
||||
buy_params = deep_merge_dicts(self._ft_params_from_file.get('buy', {}),
|
||||
getattr(self, 'buy_params', {}))
|
||||
sell_params = deep_merge_dicts(self._ft_params_from_file.get('sell', {}),
|
||||
getattr(self, 'sell_params', {}))
|
||||
protection_params = deep_merge_dicts(self._ft_params_from_file.get('protection', {}),
|
||||
getattr(self, 'protection_params', {}))
|
||||
|
||||
self._load_params(buy_params, 'buy', hyperopt)
|
||||
self._load_params(sell_params, 'sell', hyperopt)
|
||||
self._load_params(protection_params, 'protection', hyperopt)
|
||||
self._ft_load_params(buy_params, 'buy', hyperopt)
|
||||
self._ft_load_params(sell_params, 'sell', hyperopt)
|
||||
self._ft_load_params(protection_params, 'protection', hyperopt)
|
||||
|
||||
def load_params_from_file(self) -> Dict:
|
||||
filename_str = getattr(self, '__file__', '')
|
||||
|
@ -393,7 +150,7 @@ class HyperStrategyMixin:
|
|||
|
||||
return {}
|
||||
|
||||
def _load_params(self, params: Dict, space: str, hyperopt: bool = False) -> None:
|
||||
def _ft_load_params(self, params: Dict, space: str, hyperopt: bool = False) -> None:
|
||||
"""
|
||||
Set optimizable parameter values.
|
||||
:param params: Dictionary with new parameter values.
|
||||
|
@ -424,7 +181,7 @@ class HyperStrategyMixin:
|
|||
"""
|
||||
Returns list of Parameters that are not part of the current optimize job
|
||||
"""
|
||||
params = {
|
||||
params: Dict[str, Dict] = {
|
||||
'buy': {},
|
||||
'sell': {},
|
||||
'protection': {},
|
||||
|
|
|
@ -16,7 +16,7 @@ from freqtrade.enums import (CandleType, ExitCheckTuple, ExitType, SignalDirecti
|
|||
SignalType, TradingMode)
|
||||
from freqtrade.exceptions import OperationalException, StrategyError
|
||||
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date, timeframe_to_seconds
|
||||
from freqtrade.persistence import LocalTrade, Order, PairLocks, Trade
|
||||
from freqtrade.persistence import Order, PairLocks, Trade
|
||||
from freqtrade.strategy.hyper import HyperStrategyMixin
|
||||
from freqtrade.strategy.informative_decorator import (InformativeData, PopulateIndicators,
|
||||
_create_and_merge_informative_pair,
|
||||
|
@ -82,7 +82,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||
}
|
||||
|
||||
# run "populate_indicators" only for new candle
|
||||
process_only_new_candles: bool = False
|
||||
process_only_new_candles: bool = True
|
||||
|
||||
use_exit_signal: bool
|
||||
exit_profit_only: bool
|
||||
|
@ -144,6 +144,13 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||
informative_data.candle_type = config['candle_type_def']
|
||||
self._ft_informative.append((informative_data, cls_method))
|
||||
|
||||
def ft_bot_start(self, **kwargs) -> None:
|
||||
"""
|
||||
Strategy init - runs after dataprovider has been added.
|
||||
Must call bot_start()
|
||||
"""
|
||||
strategy_safe_wrapper(self.bot_start)()
|
||||
|
||||
@abstractmethod
|
||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""
|
||||
|
@ -429,7 +436,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||
return self.custom_sell(pair, trade, current_time, current_rate, current_profit, **kwargs)
|
||||
|
||||
def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
|
||||
proposed_stake: float, min_stake: float, max_stake: float,
|
||||
proposed_stake: float, min_stake: Optional[float], max_stake: float,
|
||||
entry_tag: Optional[str], side: str, **kwargs) -> float:
|
||||
"""
|
||||
Customize stake size for each new trade.
|
||||
|
@ -447,8 +454,9 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||
return proposed_stake
|
||||
|
||||
def adjust_trade_position(self, trade: Trade, current_time: datetime,
|
||||
current_rate: float, current_profit: float, min_stake: float,
|
||||
max_stake: float, **kwargs) -> Optional[float]:
|
||||
current_rate: float, current_profit: float,
|
||||
min_stake: Optional[float], max_stake: float,
|
||||
**kwargs) -> Optional[float]:
|
||||
"""
|
||||
Custom trade adjustment logic, returning the stake amount that a trade should be increased.
|
||||
This means extra buy orders with additional fees.
|
||||
|
@ -878,16 +886,16 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||
def should_exit(self, trade: Trade, rate: float, current_time: datetime, *,
|
||||
enter: bool, exit_: bool,
|
||||
low: float = None, high: float = None,
|
||||
force_stoploss: float = 0) -> ExitCheckTuple:
|
||||
force_stoploss: float = 0) -> List[ExitCheckTuple]:
|
||||
"""
|
||||
This function evaluates if one of the conditions required to trigger an exit order
|
||||
has been reached, which can either be a stop-loss, ROI or exit-signal.
|
||||
:param low: Only used during backtesting to simulate (long)stoploss/(short)ROI
|
||||
:param high: Only used during backtesting, to simulate (short)stoploss/(long)ROI
|
||||
:param force_stoploss: Externally provided stoploss
|
||||
:return: True if trade should be exited, False otherwise
|
||||
:return: List of exit reasons - or empty list.
|
||||
"""
|
||||
|
||||
exits: List[ExitCheckTuple] = []
|
||||
current_rate = rate
|
||||
current_profit = trade.calc_profit_ratio(current_rate)
|
||||
|
||||
|
@ -917,19 +925,20 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||
if exit_ and not enter:
|
||||
exit_signal = ExitType.EXIT_SIGNAL
|
||||
else:
|
||||
custom_reason = strategy_safe_wrapper(self.custom_exit, default_retval=False)(
|
||||
reason_cust = strategy_safe_wrapper(self.custom_exit, default_retval=False)(
|
||||
pair=trade.pair, trade=trade, current_time=current_time,
|
||||
current_rate=current_rate, current_profit=current_profit)
|
||||
if custom_reason:
|
||||
if reason_cust:
|
||||
exit_signal = ExitType.CUSTOM_EXIT
|
||||
if isinstance(custom_reason, str):
|
||||
if len(custom_reason) > CUSTOM_EXIT_MAX_LENGTH:
|
||||
if isinstance(reason_cust, str):
|
||||
custom_reason = reason_cust
|
||||
if len(reason_cust) > CUSTOM_EXIT_MAX_LENGTH:
|
||||
logger.warning(f'Custom exit reason returned from '
|
||||
f'custom_exit is too long and was trimmed'
|
||||
f'to {CUSTOM_EXIT_MAX_LENGTH} characters.')
|
||||
custom_reason = custom_reason[:CUSTOM_EXIT_MAX_LENGTH]
|
||||
custom_reason = reason_cust[:CUSTOM_EXIT_MAX_LENGTH]
|
||||
else:
|
||||
custom_reason = None
|
||||
custom_reason = ''
|
||||
if (
|
||||
exit_signal == ExitType.CUSTOM_EXIT
|
||||
or (exit_signal == ExitType.EXIT_SIGNAL
|
||||
|
@ -938,24 +947,29 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||
logger.debug(f"{trade.pair} - Sell signal received. "
|
||||
f"exit_type=ExitType.{exit_signal.name}" +
|
||||
(f", custom_reason={custom_reason}" if custom_reason else ""))
|
||||
return ExitCheckTuple(exit_type=exit_signal, exit_reason=custom_reason)
|
||||
exits.append(ExitCheckTuple(exit_type=exit_signal, exit_reason=custom_reason))
|
||||
|
||||
# Sequence:
|
||||
# Exit-signal
|
||||
# ROI (if not stoploss)
|
||||
# Stoploss
|
||||
if roi_reached and stoplossflag.exit_type != ExitType.STOP_LOSS:
|
||||
logger.debug(f"{trade.pair} - Required profit reached. exit_type=ExitType.ROI")
|
||||
return ExitCheckTuple(exit_type=ExitType.ROI)
|
||||
# ROI
|
||||
# Trailing stoploss
|
||||
|
||||
if stoplossflag.exit_flag:
|
||||
if stoplossflag.exit_type == ExitType.STOP_LOSS:
|
||||
|
||||
logger.debug(f"{trade.pair} - Stoploss hit. exit_type={stoplossflag.exit_type}")
|
||||
return stoplossflag
|
||||
exits.append(stoplossflag)
|
||||
|
||||
# This one is noisy, commented out...
|
||||
# logger.debug(f"{trade.pair} - No exit signal.")
|
||||
return ExitCheckTuple(exit_type=ExitType.NONE)
|
||||
if roi_reached:
|
||||
logger.debug(f"{trade.pair} - Required profit reached. exit_type=ExitType.ROI")
|
||||
exits.append(ExitCheckTuple(exit_type=ExitType.ROI))
|
||||
|
||||
if stoplossflag.exit_type == ExitType.TRAILING_STOP_LOSS:
|
||||
|
||||
logger.debug(f"{trade.pair} - Trailing stoploss hit.")
|
||||
exits.append(stoplossflag)
|
||||
|
||||
return exits
|
||||
|
||||
def stop_loss_reached(self, current_rate: float, trade: Trade,
|
||||
current_time: datetime, current_profit: float,
|
||||
|
@ -1070,7 +1084,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||
else:
|
||||
return current_profit > roi
|
||||
|
||||
def ft_check_timed_out(self, trade: LocalTrade, order: Order,
|
||||
def ft_check_timed_out(self, trade: Trade, order: Order,
|
||||
current_time: datetime) -> bool:
|
||||
"""
|
||||
FT Internal method.
|
||||
|
|
289
freqtrade/strategy/parameters.py
Normal file
289
freqtrade/strategy/parameters.py
Normal file
|
@ -0,0 +1,289 @@
|
|||
"""
|
||||
IHyperStrategy interface, hyperoptable Parameter class.
|
||||
This module defines a base class for auto-hyperoptable strategies.
|
||||
"""
|
||||
import logging
|
||||
from abc import ABC, abstractmethod
|
||||
from contextlib import suppress
|
||||
from typing import Any, Optional, Sequence, Union
|
||||
|
||||
|
||||
with suppress(ImportError):
|
||||
from skopt.space import Integer, Real, Categorical
|
||||
from freqtrade.optimize.space import SKDecimal
|
||||
|
||||
from freqtrade.exceptions import OperationalException
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BaseParameter(ABC):
|
||||
"""
|
||||
Defines a parameter that can be optimized by hyperopt.
|
||||
"""
|
||||
category: Optional[str]
|
||||
default: Any
|
||||
value: Any
|
||||
in_space: bool = False
|
||||
name: str
|
||||
|
||||
def __init__(self, *, default: Any, space: Optional[str] = None,
|
||||
optimize: bool = True, load: bool = True, **kwargs):
|
||||
"""
|
||||
Initialize hyperopt-optimizable parameter.
|
||||
:param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if
|
||||
parameter field
|
||||
name is prefixed with 'buy_' or 'sell_'.
|
||||
:param optimize: Include parameter in hyperopt optimizations.
|
||||
:param load: Load parameter value from {space}_params.
|
||||
:param kwargs: Extra parameters to skopt.space.(Integer|Real|Categorical).
|
||||
"""
|
||||
if 'name' in kwargs:
|
||||
raise OperationalException(
|
||||
'Name is determined by parameter field name and can not be specified manually.')
|
||||
self.category = space
|
||||
self._space_params = kwargs
|
||||
self.value = default
|
||||
self.optimize = optimize
|
||||
self.load = load
|
||||
|
||||
def __repr__(self):
|
||||
return f'{self.__class__.__name__}({self.value})'
|
||||
|
||||
@abstractmethod
|
||||
def get_space(self, name: str) -> Union['Integer', 'Real', 'SKDecimal', 'Categorical']:
|
||||
"""
|
||||
Get-space - will be used by Hyperopt to get the hyperopt Space
|
||||
"""
|
||||
|
||||
|
||||
class NumericParameter(BaseParameter):
|
||||
""" Internal parameter used for Numeric purposes """
|
||||
float_or_int = Union[int, float]
|
||||
default: float_or_int
|
||||
value: float_or_int
|
||||
|
||||
def __init__(self, low: Union[float_or_int, Sequence[float_or_int]],
|
||||
high: Optional[float_or_int] = None, *, default: float_or_int,
|
||||
space: Optional[str] = None, optimize: bool = True, load: bool = True, **kwargs):
|
||||
"""
|
||||
Initialize hyperopt-optimizable numeric parameter.
|
||||
Cannot be instantiated, but provides the validation for other numeric parameters
|
||||
:param low: Lower end (inclusive) of optimization space or [low, high].
|
||||
:param high: Upper end (inclusive) of optimization space.
|
||||
Must be none of entire range is passed first parameter.
|
||||
:param default: A default value.
|
||||
:param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if
|
||||
parameter fieldname is prefixed with 'buy_' or 'sell_'.
|
||||
:param optimize: Include parameter in hyperopt optimizations.
|
||||
:param load: Load parameter value from {space}_params.
|
||||
:param kwargs: Extra parameters to skopt.space.*.
|
||||
"""
|
||||
if high is not None and isinstance(low, Sequence):
|
||||
raise OperationalException(f'{self.__class__.__name__} space invalid.')
|
||||
if high is None or isinstance(low, Sequence):
|
||||
if not isinstance(low, Sequence) or len(low) != 2:
|
||||
raise OperationalException(f'{self.__class__.__name__} space must be [low, high]')
|
||||
self.low, self.high = low
|
||||
else:
|
||||
self.low = low
|
||||
self.high = high
|
||||
|
||||
super().__init__(default=default, space=space, optimize=optimize,
|
||||
load=load, **kwargs)
|
||||
|
||||
|
||||
class IntParameter(NumericParameter):
|
||||
default: int
|
||||
value: int
|
||||
low: int
|
||||
high: int
|
||||
|
||||
def __init__(self, low: Union[int, Sequence[int]], high: Optional[int] = None, *, default: int,
|
||||
space: Optional[str] = None, optimize: bool = True, load: bool = True, **kwargs):
|
||||
"""
|
||||
Initialize hyperopt-optimizable integer parameter.
|
||||
:param low: Lower end (inclusive) of optimization space or [low, high].
|
||||
:param high: Upper end (inclusive) of optimization space.
|
||||
Must be none of entire range is passed first parameter.
|
||||
:param default: A default value.
|
||||
:param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if
|
||||
parameter fieldname is prefixed with 'buy_' or 'sell_'.
|
||||
:param optimize: Include parameter in hyperopt optimizations.
|
||||
:param load: Load parameter value from {space}_params.
|
||||
:param kwargs: Extra parameters to skopt.space.Integer.
|
||||
"""
|
||||
|
||||
super().__init__(low=low, high=high, default=default, space=space, optimize=optimize,
|
||||
load=load, **kwargs)
|
||||
|
||||
def get_space(self, name: str) -> 'Integer':
|
||||
"""
|
||||
Create skopt optimization space.
|
||||
:param name: A name of parameter field.
|
||||
"""
|
||||
return Integer(low=self.low, high=self.high, name=name, **self._space_params)
|
||||
|
||||
@property
|
||||
def range(self):
|
||||
"""
|
||||
Get each value in this space as list.
|
||||
Returns a List from low to high (inclusive) in Hyperopt mode.
|
||||
Returns a List with 1 item (`value`) in "non-hyperopt" mode, to avoid
|
||||
calculating 100ds of indicators.
|
||||
"""
|
||||
if self.in_space and self.optimize:
|
||||
# Scikit-optimize ranges are "inclusive", while python's "range" is exclusive
|
||||
return range(self.low, self.high + 1)
|
||||
else:
|
||||
return range(self.value, self.value + 1)
|
||||
|
||||
|
||||
class RealParameter(NumericParameter):
|
||||
default: float
|
||||
value: float
|
||||
|
||||
def __init__(self, low: Union[float, Sequence[float]], high: Optional[float] = None, *,
|
||||
default: float, space: Optional[str] = None, optimize: bool = True,
|
||||
load: bool = True, **kwargs):
|
||||
"""
|
||||
Initialize hyperopt-optimizable floating point parameter with unlimited precision.
|
||||
:param low: Lower end (inclusive) of optimization space or [low, high].
|
||||
:param high: Upper end (inclusive) of optimization space.
|
||||
Must be none if entire range is passed first parameter.
|
||||
:param default: A default value.
|
||||
:param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if
|
||||
parameter fieldname is prefixed with 'buy_' or 'sell_'.
|
||||
:param optimize: Include parameter in hyperopt optimizations.
|
||||
:param load: Load parameter value from {space}_params.
|
||||
:param kwargs: Extra parameters to skopt.space.Real.
|
||||
"""
|
||||
super().__init__(low=low, high=high, default=default, space=space, optimize=optimize,
|
||||
load=load, **kwargs)
|
||||
|
||||
def get_space(self, name: str) -> 'Real':
|
||||
"""
|
||||
Create skopt optimization space.
|
||||
:param name: A name of parameter field.
|
||||
"""
|
||||
return Real(low=self.low, high=self.high, name=name, **self._space_params)
|
||||
|
||||
|
||||
class DecimalParameter(NumericParameter):
|
||||
default: float
|
||||
value: float
|
||||
|
||||
def __init__(self, low: Union[float, Sequence[float]], high: Optional[float] = None, *,
|
||||
default: float, decimals: int = 3, space: Optional[str] = None,
|
||||
optimize: bool = True, load: bool = True, **kwargs):
|
||||
"""
|
||||
Initialize hyperopt-optimizable decimal parameter with a limited precision.
|
||||
:param low: Lower end (inclusive) of optimization space or [low, high].
|
||||
:param high: Upper end (inclusive) of optimization space.
|
||||
Must be none if entire range is passed first parameter.
|
||||
:param default: A default value.
|
||||
:param decimals: A number of decimals after floating point to be included in testing.
|
||||
:param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if
|
||||
parameter fieldname is prefixed with 'buy_' or 'sell_'.
|
||||
:param optimize: Include parameter in hyperopt optimizations.
|
||||
:param load: Load parameter value from {space}_params.
|
||||
:param kwargs: Extra parameters to skopt.space.Integer.
|
||||
"""
|
||||
self._decimals = decimals
|
||||
default = round(default, self._decimals)
|
||||
|
||||
super().__init__(low=low, high=high, default=default, space=space, optimize=optimize,
|
||||
load=load, **kwargs)
|
||||
|
||||
def get_space(self, name: str) -> 'SKDecimal':
|
||||
"""
|
||||
Create skopt optimization space.
|
||||
:param name: A name of parameter field.
|
||||
"""
|
||||
return SKDecimal(low=self.low, high=self.high, decimals=self._decimals, name=name,
|
||||
**self._space_params)
|
||||
|
||||
@property
|
||||
def range(self):
|
||||
"""
|
||||
Get each value in this space as list.
|
||||
Returns a List from low to high (inclusive) in Hyperopt mode.
|
||||
Returns a List with 1 item (`value`) in "non-hyperopt" mode, to avoid
|
||||
calculating 100ds of indicators.
|
||||
"""
|
||||
if self.in_space and self.optimize:
|
||||
low = int(self.low * pow(10, self._decimals))
|
||||
high = int(self.high * pow(10, self._decimals)) + 1
|
||||
return [round(n * pow(0.1, self._decimals), self._decimals) for n in range(low, high)]
|
||||
else:
|
||||
return [self.value]
|
||||
|
||||
|
||||
class CategoricalParameter(BaseParameter):
|
||||
default: Any
|
||||
value: Any
|
||||
opt_range: Sequence[Any]
|
||||
|
||||
def __init__(self, categories: Sequence[Any], *, default: Optional[Any] = None,
|
||||
space: Optional[str] = None, optimize: bool = True, load: bool = True, **kwargs):
|
||||
"""
|
||||
Initialize hyperopt-optimizable parameter.
|
||||
:param categories: Optimization space, [a, b, ...].
|
||||
:param default: A default value. If not specified, first item from specified space will be
|
||||
used.
|
||||
:param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if
|
||||
parameter field
|
||||
name is prefixed with 'buy_' or 'sell_'.
|
||||
:param optimize: Include parameter in hyperopt optimizations.
|
||||
:param load: Load parameter value from {space}_params.
|
||||
:param kwargs: Extra parameters to skopt.space.Categorical.
|
||||
"""
|
||||
if len(categories) < 2:
|
||||
raise OperationalException(
|
||||
'CategoricalParameter space must be [a, b, ...] (at least two parameters)')
|
||||
self.opt_range = categories
|
||||
super().__init__(default=default, space=space, optimize=optimize,
|
||||
load=load, **kwargs)
|
||||
|
||||
def get_space(self, name: str) -> 'Categorical':
|
||||
"""
|
||||
Create skopt optimization space.
|
||||
:param name: A name of parameter field.
|
||||
"""
|
||||
return Categorical(self.opt_range, name=name, **self._space_params)
|
||||
|
||||
@property
|
||||
def range(self):
|
||||
"""
|
||||
Get each value in this space as list.
|
||||
Returns a List of categories in Hyperopt mode.
|
||||
Returns a List with 1 item (`value`) in "non-hyperopt" mode, to avoid
|
||||
calculating 100ds of indicators.
|
||||
"""
|
||||
if self.in_space and self.optimize:
|
||||
return self.opt_range
|
||||
else:
|
||||
return [self.value]
|
||||
|
||||
|
||||
class BooleanParameter(CategoricalParameter):
|
||||
|
||||
def __init__(self, *, default: Optional[Any] = None,
|
||||
space: Optional[str] = None, optimize: bool = True, load: bool = True, **kwargs):
|
||||
"""
|
||||
Initialize hyperopt-optimizable Boolean Parameter.
|
||||
It's a shortcut to `CategoricalParameter([True, False])`.
|
||||
:param default: A default value. If not specified, first item from specified space will be
|
||||
used.
|
||||
:param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if
|
||||
parameter field
|
||||
name is prefixed with 'buy_' or 'sell_'.
|
||||
:param optimize: Include parameter in hyperopt optimizations.
|
||||
:param load: Load parameter value from {space}_params.
|
||||
:param kwargs: Extra parameters to skopt.space.Categorical.
|
||||
"""
|
||||
|
||||
categories = [True, False]
|
||||
super().__init__(categories=categories, default=default, space=space, optimize=optimize,
|
||||
load=load, **kwargs)
|
|
@ -1,5 +1,7 @@
|
|||
import logging
|
||||
from copy import deepcopy
|
||||
from functools import wraps
|
||||
from typing import Any, Callable, TypeVar, cast
|
||||
|
||||
from freqtrade.exceptions import StrategyError
|
||||
|
||||
|
@ -7,12 +9,16 @@ from freqtrade.exceptions import StrategyError
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def strategy_safe_wrapper(f, message: str = "", default_retval=None, supress_error=False):
|
||||
F = TypeVar('F', bound=Callable[..., Any])
|
||||
|
||||
|
||||
def strategy_safe_wrapper(f: F, message: str = "", default_retval=None, supress_error=False) -> F:
|
||||
"""
|
||||
Wrapper around user-provided methods and functions.
|
||||
Caches all exceptions and returns either the default_retval (if it's not None) or raises
|
||||
a StrategyError exception, which then needs to be handled by the calling method.
|
||||
"""
|
||||
@wraps(f)
|
||||
def wrapper(*args, **kwargs):
|
||||
try:
|
||||
if 'trade' in kwargs:
|
||||
|
@ -37,4 +43,4 @@ def strategy_safe_wrapper(f, message: str = "", default_retval=None, supress_err
|
|||
raise StrategyError(str(error)) from error
|
||||
return default_retval
|
||||
|
||||
return wrapper
|
||||
return cast(F, wrapper)
|
||||
|
|
|
@ -6,7 +6,7 @@ import numpy as np # noqa
|
|||
import pandas as pd # noqa
|
||||
from pandas import DataFrame # noqa
|
||||
from datetime import datetime # noqa
|
||||
from typing import Optional # noqa
|
||||
from typing import Optional, Union # noqa
|
||||
|
||||
from freqtrade.strategy import (BooleanParameter, CategoricalParameter, DecimalParameter,
|
||||
IStrategy, IntParameter)
|
||||
|
@ -64,7 +64,7 @@ class {{ strategy }}(IStrategy):
|
|||
# trailing_stop_positive_offset = 0.0 # Disabled / not configured
|
||||
|
||||
# Run "populate_indicators()" only for new candle.
|
||||
process_only_new_candles = False
|
||||
process_only_new_candles = True
|
||||
|
||||
# These values can be overridden in the config.
|
||||
use_exit_signal = True
|
||||
|
|
|
@ -62,7 +62,7 @@ class SampleStrategy(IStrategy):
|
|||
timeframe = '5m'
|
||||
|
||||
# Run "populate_indicators()" only for new candle.
|
||||
process_only_new_candles = False
|
||||
process_only_new_candles = True
|
||||
|
||||
# These values can be overridden in the config.
|
||||
use_exit_signal = True
|
||||
|
|
|
@ -13,7 +13,7 @@ def bot_loop_start(self, **kwargs) -> None:
|
|||
pass
|
||||
|
||||
def custom_entry_price(self, pair: str, current_time: 'datetime', proposed_rate: float,
|
||||
entry_tag: Optional[str], **kwargs) -> float:
|
||||
entry_tag: 'Optional[str]', side: str, **kwargs) -> float:
|
||||
"""
|
||||
Custom entry price logic, returning the new entry price.
|
||||
|
||||
|
@ -80,8 +80,8 @@ def custom_exit_price(self, pair: str, trade: 'Trade',
|
|||
return proposed_rate
|
||||
|
||||
def custom_stake_amount(self, pair: str, current_time: 'datetime', current_rate: float,
|
||||
proposed_stake: float, min_stake: float, max_stake: float,
|
||||
side: str, entry_tag: Optional[str], **kwargs) -> float:
|
||||
proposed_stake: float, min_stake: Optional[float], max_stake: float,
|
||||
entry_tag: 'Optional[str]', side: str, **kwargs) -> float:
|
||||
"""
|
||||
Customize stake size for each new trade.
|
||||
|
||||
|
@ -244,8 +244,8 @@ def check_exit_timeout(self, pair: str, trade: 'Trade', order: 'Order',
|
|||
return False
|
||||
|
||||
def adjust_trade_position(self, trade: 'Trade', current_time: 'datetime',
|
||||
current_rate: float, current_profit: float, min_stake: float,
|
||||
max_stake: float, **kwargs) -> Optional[float]:
|
||||
current_rate: float, current_profit: float, min_stake: Optional[float],
|
||||
max_stake: float, **kwargs) -> 'Optional[float]':
|
||||
"""
|
||||
Custom trade adjustment logic, returning the stake amount that a trade should be increased.
|
||||
This means extra buy orders with additional fees.
|
||||
|
|
|
@ -28,6 +28,28 @@ skip_glob = ["**/.env*", "**/env/*", "**/.venv/*", "**/docs/*", "**/user_data/*"
|
|||
[tool.pytest.ini_options]
|
||||
asyncio_mode = "auto"
|
||||
|
||||
[tool.mypy]
|
||||
ignore_missing_imports = true
|
||||
warn_unused_ignores = true
|
||||
exclude = [
|
||||
'^build_helpers\.py$'
|
||||
]
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
module = "tests.*"
|
||||
ignore_errors = true
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools >= 46.4.0", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[tool.pyright]
|
||||
include = ["freqtrade"]
|
||||
exclude = [
|
||||
"**/__pycache__",
|
||||
"build_helpers/*.py",
|
||||
]
|
||||
ignore = ["freqtrade/vendor/**"]
|
||||
|
||||
# Align pyright to mypy config
|
||||
strictParameterNoneValue = false
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
coveralls==3.3.1
|
||||
flake8==4.0.1
|
||||
flake8-tidy-imports==4.8.0
|
||||
mypy==0.950
|
||||
mypy==0.960
|
||||
pre-commit==2.19.0
|
||||
pytest==7.1.2
|
||||
pytest-asyncio==0.18.3
|
||||
|
@ -23,7 +23,7 @@ nbconvert==6.5.0
|
|||
|
||||
# mypy types
|
||||
types-cachetools==5.0.1
|
||||
types-filelock==3.2.5
|
||||
types-requests==2.27.25
|
||||
types-filelock==3.2.6
|
||||
types-requests==2.27.29
|
||||
types-tabulate==0.8.9
|
||||
types-python-dateutil==2.8.15
|
||||
types-python-dateutil==2.8.17
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
-r requirements.txt
|
||||
|
||||
# Required for hyperopt
|
||||
scipy==1.8.0
|
||||
scikit-learn==1.1.0
|
||||
scipy==1.8.1
|
||||
scikit-learn==1.1.1
|
||||
scikit-optimize==0.9.0
|
||||
filelock==3.7.0
|
||||
progressbar2==4.0.0
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
numpy==1.22.3
|
||||
numpy==1.22.4
|
||||
pandas==1.4.2
|
||||
pandas-ta==0.3.14b
|
||||
|
||||
ccxt==1.82.61
|
||||
ccxt==1.84.39
|
||||
# Pin cryptography for now due to rust build errors with piwheels
|
||||
cryptography==37.0.2
|
||||
aiohttp==3.8.1
|
||||
SQLAlchemy==1.4.36
|
||||
python-telegram-bot==13.11
|
||||
python-telegram-bot==13.12
|
||||
arrow==1.2.2
|
||||
cachetools==4.2.2
|
||||
requests==2.27.1
|
||||
|
@ -38,7 +38,7 @@ fastapi==0.78.0
|
|||
uvicorn==0.17.6
|
||||
pyjwt==2.4.0
|
||||
aiofiles==0.8.0
|
||||
psutil==5.9.0
|
||||
psutil==5.9.1
|
||||
|
||||
# Support for colorized terminal output
|
||||
colorama==0.4.4
|
||||
|
|
10
setup.cfg
10
setup.cfg
|
@ -50,13 +50,3 @@ exclude =
|
|||
.eggs,
|
||||
user_data,
|
||||
|
||||
[mypy]
|
||||
ignore_missing_imports = True
|
||||
warn_unused_ignores = True
|
||||
exclude = (?x)(
|
||||
^build_helpers\.py$
|
||||
)
|
||||
|
||||
|
||||
[mypy-tests.*]
|
||||
ignore_errors = True
|
||||
|
|
2
setup.py
2
setup.py
|
@ -42,7 +42,7 @@ setup(
|
|||
],
|
||||
install_requires=[
|
||||
# from requirements.txt
|
||||
'ccxt>=1.80.67',
|
||||
'ccxt>=1.83.12',
|
||||
'SQLAlchemy',
|
||||
'python-telegram-bot>=13.4',
|
||||
'arrow>=0.17.0',
|
||||
|
|
|
@ -1495,7 +1495,7 @@ def test_start_convert_db(mocker, fee, tmpdir, caplog):
|
|||
]
|
||||
|
||||
assert not db_src_file.is_file()
|
||||
init_db(db_from, False)
|
||||
init_db(db_from)
|
||||
|
||||
create_mock_trades(fee)
|
||||
|
||||
|
|
|
@ -384,7 +384,7 @@ def patch_coingekko(mocker) -> None:
|
|||
|
||||
@pytest.fixture(scope='function')
|
||||
def init_persistence(default_conf):
|
||||
init_db(default_conf['db_url'], default_conf['dry_run'])
|
||||
init_db(default_conf['db_url'])
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
|
@ -1616,6 +1616,7 @@ def limit_buy_order_open():
|
|||
'datetime': arrow.utcnow().isoformat(),
|
||||
'price': 0.00001099,
|
||||
'amount': 90.99181073,
|
||||
'average': None,
|
||||
'filled': 0.0,
|
||||
'cost': 0.0009999,
|
||||
'remaining': 90.99181073,
|
||||
|
|
|
@ -154,6 +154,7 @@ def test_stoploss_adjust_binance(mocker, default_conf, sl1, sl2, sl3, side):
|
|||
order = {
|
||||
'type': 'stop_loss_limit',
|
||||
'price': 1500,
|
||||
'stopPrice': 1500,
|
||||
'info': {'stopPrice': 1500},
|
||||
}
|
||||
assert exchange.stoploss_adjust(sl1, order, side=side)
|
||||
|
@ -490,11 +491,11 @@ def test_fill_leverage_tiers_binance_dryrun(default_conf, mocker, leverage_tiers
|
|||
default_conf['margin_mode'] = MarginMode.ISOLATED
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance")
|
||||
exchange.fill_leverage_tiers()
|
||||
|
||||
leverage_tiers = leverage_tiers
|
||||
|
||||
assert len(exchange._leverage_tiers.keys()) > 100
|
||||
for key, value in leverage_tiers.items():
|
||||
assert exchange._leverage_tiers[key] == value
|
||||
v = exchange._leverage_tiers[key]
|
||||
assert isinstance(v, list)
|
||||
assert len(v) == len(value)
|
||||
|
||||
|
||||
def test__set_leverage_binance(mocker, default_conf):
|
||||
|
|
|
@ -2155,6 +2155,8 @@ async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_
|
|||
|
||||
@pytest.mark.asyncio
|
||||
async def test__async_kucoin_get_candle_history(default_conf, mocker, caplog):
|
||||
from freqtrade.exchange.common import _reset_logging_mixin
|
||||
_reset_logging_mixin()
|
||||
caplog.set_level(logging.INFO)
|
||||
api_mock = MagicMock()
|
||||
api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.DDoSProtection(
|
||||
|
@ -2808,6 +2810,7 @@ def test_get_historic_trades_notsupported(default_conf, mocker, caplog, exchange
|
|||
until=trades_history[-1][0])
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_persistence")
|
||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||
def test_cancel_order_dry_run(default_conf, mocker, exchange_name):
|
||||
default_conf['dry_run'] = True
|
||||
|
@ -2973,6 +2976,7 @@ def test_cancel_stoploss_order_with_result(default_conf, mocker, exchange_name):
|
|||
exchange.cancel_stoploss_order_with_result(order_id='_', pair='TKN/BTC', amount=123)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_persistence")
|
||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||
def test_fetch_order(default_conf, mocker, exchange_name, caplog):
|
||||
default_conf['dry_run'] = True
|
||||
|
@ -3025,6 +3029,7 @@ def test_fetch_order(default_conf, mocker, exchange_name, caplog):
|
|||
order_id='_', pair='TKN/BTC')
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_persistence")
|
||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||
def test_fetch_stoploss_order(default_conf, mocker, exchange_name):
|
||||
# Don't test FTX here - that needs a separate test
|
||||
|
@ -3814,6 +3819,7 @@ def test_validate_trading_mode_and_margin_mode(
|
|||
("bibox", "spot", {"has": {"fetchCurrencies": False}}),
|
||||
("bibox", "margin", {"has": {"fetchCurrencies": False}, "options": {"defaultType": "margin"}}),
|
||||
("bibox", "futures", {"has": {"fetchCurrencies": False}, "options": {"defaultType": "swap"}}),
|
||||
("bybit", "spot", {"options": {"defaultType": "spot"}}),
|
||||
("bybit", "futures", {"options": {"defaultType": "linear"}}),
|
||||
("ftx", "futures", {"options": {"defaultType": "swap"}}),
|
||||
("gateio", "futures", {"options": {"defaultType": "swap"}}),
|
||||
|
@ -3912,6 +3918,70 @@ def test_calculate_funding_fees(
|
|||
) == kraken_fee
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'mark_price,funding_rate,futures_funding_rate', [
|
||||
(1000, 0.001, None),
|
||||
(1000, 0.001, 0.01),
|
||||
(1000, 0.001, 0.0),
|
||||
(1000, 0.001, -0.01),
|
||||
])
|
||||
def test_combine_funding_and_mark(
|
||||
default_conf,
|
||||
mocker,
|
||||
funding_rate,
|
||||
mark_price,
|
||||
futures_funding_rate,
|
||||
):
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
prior2_date = timeframe_to_prev_date('1h', datetime.now(timezone.utc) - timedelta(hours=2))
|
||||
prior_date = timeframe_to_prev_date('1h', datetime.now(timezone.utc) - timedelta(hours=1))
|
||||
trade_date = timeframe_to_prev_date('1h', datetime.now(timezone.utc))
|
||||
funding_rates = DataFrame([
|
||||
{'date': prior2_date, 'open': funding_rate},
|
||||
{'date': prior_date, 'open': funding_rate},
|
||||
{'date': trade_date, 'open': funding_rate},
|
||||
])
|
||||
mark_rates = DataFrame([
|
||||
{'date': prior2_date, 'open': mark_price},
|
||||
{'date': prior_date, 'open': mark_price},
|
||||
{'date': trade_date, 'open': mark_price},
|
||||
])
|
||||
|
||||
df = exchange.combine_funding_and_mark(funding_rates, mark_rates, futures_funding_rate)
|
||||
assert 'open_mark' in df.columns
|
||||
assert 'open_fund' in df.columns
|
||||
assert len(df) == 3
|
||||
|
||||
funding_rates = DataFrame([
|
||||
{'date': trade_date, 'open': funding_rate},
|
||||
])
|
||||
mark_rates = DataFrame([
|
||||
{'date': prior2_date, 'open': mark_price},
|
||||
{'date': prior_date, 'open': mark_price},
|
||||
{'date': trade_date, 'open': mark_price},
|
||||
])
|
||||
df = exchange.combine_funding_and_mark(funding_rates, mark_rates, futures_funding_rate)
|
||||
|
||||
if futures_funding_rate is not None:
|
||||
assert len(df) == 3
|
||||
assert df.iloc[0]['open_fund'] == futures_funding_rate
|
||||
assert df.iloc[1]['open_fund'] == futures_funding_rate
|
||||
assert df.iloc[2]['open_fund'] == funding_rate
|
||||
else:
|
||||
assert len(df) == 1
|
||||
|
||||
# Empty funding rates
|
||||
funding_rates = DataFrame([], columns=['date', 'open'])
|
||||
df = exchange.combine_funding_and_mark(funding_rates, mark_rates, futures_funding_rate)
|
||||
if futures_funding_rate is not None:
|
||||
assert len(df) == 3
|
||||
assert df.iloc[0]['open_fund'] == futures_funding_rate
|
||||
assert df.iloc[1]['open_fund'] == futures_funding_rate
|
||||
assert df.iloc[2]['open_fund'] == futures_funding_rate
|
||||
else:
|
||||
assert len(df) == 0
|
||||
|
||||
|
||||
def test_get_or_calculate_liquidation_price(mocker, default_conf):
|
||||
|
||||
api_mock = MagicMock()
|
||||
|
|
|
@ -174,6 +174,7 @@ def test_stoploss_adjust_ftx(mocker, default_conf, sl1, sl2, sl3, side):
|
|||
assert not exchange.stoploss_adjust(sl3, order, side=side)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_persistence")
|
||||
def test_fetch_stoploss_order_ftx(default_conf, mocker, limit_sell_order, limit_buy_order):
|
||||
default_conf['dry_run'] = True
|
||||
order = MagicMock()
|
||||
|
|
|
@ -34,6 +34,7 @@ def test_validate_order_types_gateio(default_conf, mocker):
|
|||
ExchangeResolver.load_exchange('gateio', default_conf, True)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_persistence")
|
||||
def test_fetch_stoploss_order_gateio(default_conf, mocker):
|
||||
exchange = get_patched_exchange(mocker, default_conf, id='gateio')
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ def test_backtest_position_adjustment(default_conf, fee, mocker, testdatadir) ->
|
|||
default_conf.update({
|
||||
"stake_amount": 100.0,
|
||||
"dry_run_wallet": 1000.0,
|
||||
"strategy": "StrategyTestV2"
|
||||
"strategy": "StrategyTestV3"
|
||||
})
|
||||
backtesting = Backtesting(default_conf)
|
||||
backtesting._set_strategy(backtesting.strategylist[0])
|
||||
|
|
|
@ -17,7 +17,7 @@ from freqtrade.optimize.hyperopt_auto import HyperOptAuto
|
|||
from freqtrade.optimize.hyperopt_tools import HyperoptTools
|
||||
from freqtrade.optimize.optimize_reports import generate_strategy_stats
|
||||
from freqtrade.optimize.space import SKDecimal
|
||||
from freqtrade.strategy.hyper import IntParameter
|
||||
from freqtrade.strategy import IntParameter
|
||||
from tests.conftest import (CURRENT_TEST_STRATEGY, get_args, log_has, log_has_re, patch_exchange,
|
||||
patched_configuration_load_config_file)
|
||||
|
||||
|
|
|
@ -972,6 +972,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets, is_short,
|
|||
'exchange': 'binance',
|
||||
'leverage': 1.0,
|
||||
'interest_rate': 0.0,
|
||||
'liquidation_price': None,
|
||||
'funding_fees': None,
|
||||
'trading_mode': ANY,
|
||||
'orders': [ANY],
|
||||
|
@ -1175,6 +1176,7 @@ def test_api_force_entry(botclient, mocker, fee, endpoint):
|
|||
'exchange': 'binance',
|
||||
'leverage': None,
|
||||
'interest_rate': None,
|
||||
'liquidation_price': None,
|
||||
'funding_fees': None,
|
||||
'trading_mode': 'spot',
|
||||
'orders': [],
|
||||
|
|
|
@ -1,12 +1,9 @@
|
|||
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
import talib.abstract as ta
|
||||
from pandas import DataFrame
|
||||
|
||||
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.strategy import IStrategy
|
||||
|
||||
|
||||
|
@ -149,12 +146,3 @@ class StrategyTestV2(IStrategy):
|
|||
),
|
||||
'sell'] = 1
|
||||
return dataframe
|
||||
|
||||
def adjust_trade_position(self, trade: Trade, current_time: datetime, current_rate: float,
|
||||
current_profit: float, min_stake: float, max_stake: float, **kwargs):
|
||||
|
||||
if current_profit < -0.0075:
|
||||
orders = trade.select_filled_orders('buy')
|
||||
return round(orders[0].cost, 0)
|
||||
|
||||
return None
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
import talib.abstract as ta
|
||||
from pandas import DataFrame
|
||||
|
@ -185,7 +186,8 @@ class StrategyTestV3(IStrategy):
|
|||
return 3.0
|
||||
|
||||
def adjust_trade_position(self, trade: Trade, current_time: datetime, current_rate: float,
|
||||
current_profit: float, min_stake: float, max_stake: float, **kwargs):
|
||||
current_profit: float,
|
||||
min_stake: Optional[float], max_stake: float, **kwargs):
|
||||
|
||||
if current_profit < -0.0075:
|
||||
orders = trade.select_filled_orders(trade.entry_side)
|
||||
|
|
|
@ -16,8 +16,8 @@ from freqtrade.exceptions import OperationalException, StrategyError
|
|||
from freqtrade.optimize.space import SKDecimal
|
||||
from freqtrade.persistence import PairLocks, Trade
|
||||
from freqtrade.resolvers import StrategyResolver
|
||||
from freqtrade.strategy.hyper import (BaseParameter, BooleanParameter, CategoricalParameter,
|
||||
DecimalParameter, IntParameter, RealParameter)
|
||||
from freqtrade.strategy.parameters import (BaseParameter, BooleanParameter, CategoricalParameter,
|
||||
DecimalParameter, IntParameter, RealParameter)
|
||||
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
|
||||
from tests.conftest import CURRENT_TEST_STRATEGY, TRADE_SIDES, log_has, log_has_re
|
||||
|
||||
|
@ -495,37 +495,113 @@ def test_custom_exit(default_conf, fee, caplog) -> None:
|
|||
enter=False, exit_=False,
|
||||
low=None, high=None)
|
||||
|
||||
assert res.exit_flag is False
|
||||
assert res.exit_type == ExitType.NONE
|
||||
assert res == []
|
||||
|
||||
strategy.custom_exit = MagicMock(return_value=True)
|
||||
res = strategy.should_exit(trade, 1, now,
|
||||
enter=False, exit_=False,
|
||||
low=None, high=None)
|
||||
assert res.exit_flag is True
|
||||
assert res.exit_type == ExitType.CUSTOM_EXIT
|
||||
assert res.exit_reason == 'custom_exit'
|
||||
assert res[0].exit_flag is True
|
||||
assert res[0].exit_type == ExitType.CUSTOM_EXIT
|
||||
assert res[0].exit_reason == 'custom_exit'
|
||||
|
||||
strategy.custom_exit = MagicMock(return_value='hello world')
|
||||
|
||||
res = strategy.should_exit(trade, 1, now,
|
||||
enter=False, exit_=False,
|
||||
low=None, high=None)
|
||||
assert res.exit_type == ExitType.CUSTOM_EXIT
|
||||
assert res.exit_flag is True
|
||||
assert res.exit_reason == 'hello world'
|
||||
assert res[0].exit_type == ExitType.CUSTOM_EXIT
|
||||
assert res[0].exit_flag is True
|
||||
assert res[0].exit_reason == 'hello world'
|
||||
|
||||
caplog.clear()
|
||||
strategy.custom_exit = MagicMock(return_value='h' * 100)
|
||||
res = strategy.should_exit(trade, 1, now,
|
||||
enter=False, exit_=False,
|
||||
low=None, high=None)
|
||||
assert res.exit_type == ExitType.CUSTOM_EXIT
|
||||
assert res.exit_flag is True
|
||||
assert res.exit_reason == 'h' * 64
|
||||
assert res[0].exit_type == ExitType.CUSTOM_EXIT
|
||||
assert res[0].exit_flag is True
|
||||
assert res[0].exit_reason == 'h' * 64
|
||||
assert log_has_re('Custom exit reason returned from custom_exit is too long.*', caplog)
|
||||
|
||||
|
||||
def test_should_sell(default_conf, fee) -> None:
|
||||
|
||||
strategy = StrategyResolver.load_strategy(default_conf)
|
||||
trade = Trade(
|
||||
pair='ETH/BTC',
|
||||
stake_amount=0.01,
|
||||
amount=1,
|
||||
open_date=arrow.utcnow().shift(hours=-1).datetime,
|
||||
fee_open=fee.return_value,
|
||||
fee_close=fee.return_value,
|
||||
exchange='binance',
|
||||
open_rate=1,
|
||||
)
|
||||
now = arrow.utcnow().datetime
|
||||
res = strategy.should_exit(trade, 1, now,
|
||||
enter=False, exit_=False,
|
||||
low=None, high=None)
|
||||
|
||||
assert res == []
|
||||
strategy.min_roi_reached = MagicMock(return_value=True)
|
||||
|
||||
res = strategy.should_exit(trade, 1, now,
|
||||
enter=False, exit_=False,
|
||||
low=None, high=None)
|
||||
assert len(res) == 1
|
||||
assert res == [ExitCheckTuple(exit_type=ExitType.ROI)]
|
||||
|
||||
strategy.min_roi_reached = MagicMock(return_value=True)
|
||||
strategy.stop_loss_reached = MagicMock(
|
||||
return_value=ExitCheckTuple(exit_type=ExitType.STOP_LOSS))
|
||||
|
||||
res = strategy.should_exit(trade, 1, now,
|
||||
enter=False, exit_=False,
|
||||
low=None, high=None)
|
||||
assert len(res) == 2
|
||||
assert res == [
|
||||
ExitCheckTuple(exit_type=ExitType.STOP_LOSS),
|
||||
ExitCheckTuple(exit_type=ExitType.ROI),
|
||||
]
|
||||
|
||||
strategy.custom_exit = MagicMock(return_value='hello world')
|
||||
# custom-exit and exit-signal is first
|
||||
res = strategy.should_exit(trade, 1, now,
|
||||
enter=False, exit_=False,
|
||||
low=None, high=None)
|
||||
assert len(res) == 3
|
||||
assert res == [
|
||||
ExitCheckTuple(exit_type=ExitType.CUSTOM_EXIT, exit_reason='hello world'),
|
||||
ExitCheckTuple(exit_type=ExitType.STOP_LOSS),
|
||||
ExitCheckTuple(exit_type=ExitType.ROI),
|
||||
]
|
||||
|
||||
strategy.stop_loss_reached = MagicMock(
|
||||
return_value=ExitCheckTuple(exit_type=ExitType.TRAILING_STOP_LOSS))
|
||||
# Regular exit signal
|
||||
res = strategy.should_exit(trade, 1, now,
|
||||
enter=False, exit_=True,
|
||||
low=None, high=None)
|
||||
assert len(res) == 3
|
||||
assert res == [
|
||||
ExitCheckTuple(exit_type=ExitType.EXIT_SIGNAL),
|
||||
ExitCheckTuple(exit_type=ExitType.ROI),
|
||||
ExitCheckTuple(exit_type=ExitType.TRAILING_STOP_LOSS),
|
||||
]
|
||||
|
||||
# Regular exit signal, no ROI
|
||||
strategy.min_roi_reached = MagicMock(return_value=False)
|
||||
res = strategy.should_exit(trade, 1, now,
|
||||
enter=False, exit_=True,
|
||||
low=None, high=None)
|
||||
assert len(res) == 2
|
||||
assert res == [
|
||||
ExitCheckTuple(exit_type=ExitType.EXIT_SIGNAL),
|
||||
ExitCheckTuple(exit_type=ExitType.TRAILING_STOP_LOSS),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize('side', TRADE_SIDES)
|
||||
def test_leverage_callback(default_conf, side) -> None:
|
||||
default_conf['strategy'] = 'StrategyTestV2'
|
||||
|
|
|
@ -224,12 +224,12 @@ def test_strategy_override_process_only_new_candles(caplog, default_conf):
|
|||
|
||||
default_conf.update({
|
||||
'strategy': CURRENT_TEST_STRATEGY,
|
||||
'process_only_new_candles': True
|
||||
'process_only_new_candles': False
|
||||
})
|
||||
strategy = StrategyResolver.load_strategy(default_conf)
|
||||
|
||||
assert strategy.process_only_new_candles
|
||||
assert log_has("Override strategy 'process_only_new_candles' with value in config file: True.",
|
||||
assert not strategy.process_only_new_candles
|
||||
assert log_has("Override strategy 'process_only_new_candles' with value in config file: False.",
|
||||
caplog)
|
||||
|
||||
|
||||
|
|
|
@ -1775,9 +1775,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog,
|
|||
'type': 'stop_loss_limit',
|
||||
'price': 3,
|
||||
'average': 2,
|
||||
'info': {
|
||||
'stopPrice': '2.178'
|
||||
}
|
||||
'stopPrice': '2.178'
|
||||
})
|
||||
|
||||
mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_order_hanging)
|
||||
|
@ -3044,6 +3042,7 @@ def test_handle_cancel_enter_corder_empty(mocker, default_conf_usdt, limit_order
|
|||
trade.entry_side = "buy"
|
||||
trade.open_rate = 200
|
||||
trade.entry_side = "buy"
|
||||
trade.open_order_id = "open_order_noop"
|
||||
l_order['filled'] = 0.0
|
||||
l_order['status'] = 'open'
|
||||
reason = CANCEL_REASON['TIMEOUT']
|
||||
|
@ -3581,7 +3580,7 @@ def test_may_execute_trade_exit_after_stoploss_on_exchange_hit(
|
|||
assert rpc_mock.call_count == 3
|
||||
assert rpc_mock.call_args_list[0][0][0]['type'] == RPCMessageType.ENTRY
|
||||
assert rpc_mock.call_args_list[1][0][0]['type'] == RPCMessageType.ENTRY_FILL
|
||||
assert rpc_mock.call_args_list[2][0][0]['type'] == RPCMessageType.EXIT
|
||||
assert rpc_mock.call_args_list[2][0][0]['type'] == RPCMessageType.EXIT_FILL
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
@ -4786,9 +4785,6 @@ def test_startup_update_open_orders(mocker, default_conf_usdt, fee, caplog, is_s
|
|||
freqtrade.config['dry_run'] = False
|
||||
freqtrade.startup_update_open_orders()
|
||||
|
||||
assert log_has_re(r"Error updating Order .*", caplog)
|
||||
caplog.clear()
|
||||
|
||||
assert len(Order.get_open_orders()) == 3
|
||||
matching_buy_order = mock_order_4(is_short=is_short)
|
||||
matching_buy_order.update({
|
||||
|
@ -4799,6 +4795,11 @@ def test_startup_update_open_orders(mocker, default_conf_usdt, fee, caplog, is_s
|
|||
# Only stoploss and sell orders are kept open
|
||||
assert len(Order.get_open_orders()) == 2
|
||||
|
||||
caplog.clear()
|
||||
mocker.patch('freqtrade.exchange.Exchange.fetch_order', side_effect=InvalidOrderException)
|
||||
freqtrade.startup_update_open_orders()
|
||||
assert log_has_re(r"Error updating Order .*", caplog)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_persistence")
|
||||
@pytest.mark.parametrize("is_short", [False, True])
|
||||
|
|
|
@ -52,8 +52,8 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee,
|
|||
side_effect=[stoploss_order_closed, stoploss_order_open, stoploss_order_open])
|
||||
# Sell 3rd trade (not called for the first trade)
|
||||
should_sell_mock = MagicMock(side_effect=[
|
||||
ExitCheckTuple(exit_type=ExitType.NONE),
|
||||
ExitCheckTuple(exit_type=ExitType.EXIT_SIGNAL)]
|
||||
[],
|
||||
[ExitCheckTuple(exit_type=ExitType.EXIT_SIGNAL)]]
|
||||
)
|
||||
cancel_order_mock = MagicMock()
|
||||
mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss)
|
||||
|
@ -160,11 +160,11 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, mocker, balance_rati
|
|||
_notify_exit=MagicMock(),
|
||||
)
|
||||
should_sell_mock = MagicMock(side_effect=[
|
||||
ExitCheckTuple(exit_type=ExitType.NONE),
|
||||
ExitCheckTuple(exit_type=ExitType.EXIT_SIGNAL),
|
||||
ExitCheckTuple(exit_type=ExitType.NONE),
|
||||
ExitCheckTuple(exit_type=ExitType.NONE),
|
||||
ExitCheckTuple(exit_type=ExitType.NONE)]
|
||||
[],
|
||||
[ExitCheckTuple(exit_type=ExitType.EXIT_SIGNAL)],
|
||||
[],
|
||||
[],
|
||||
[]]
|
||||
)
|
||||
mocker.patch("freqtrade.strategy.interface.IStrategy.should_exit", should_sell_mock)
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ from sqlalchemy import create_engine, text
|
|||
from freqtrade import constants
|
||||
from freqtrade.enums import TradingMode
|
||||
from freqtrade.exceptions import DependencyException, OperationalException
|
||||
from freqtrade.persistence import LocalTrade, Order, Trade, clean_dry_run_db, init_db
|
||||
from freqtrade.persistence import LocalTrade, Order, Trade, init_db
|
||||
from freqtrade.persistence.migrations import get_last_sequence_ids, set_sequence_ids
|
||||
from freqtrade.persistence.models import PairLock
|
||||
from tests.conftest import create_mock_trades, create_mock_trades_with_leverage, log_has, log_has_re
|
||||
|
@ -24,7 +24,7 @@ spot, margin, futures = TradingMode.SPOT, TradingMode.MARGIN, TradingMode.FUTURE
|
|||
|
||||
def test_init_create_session(default_conf):
|
||||
# Check if init create a session
|
||||
init_db(default_conf['db_url'], default_conf['dry_run'])
|
||||
init_db(default_conf['db_url'])
|
||||
assert hasattr(Trade, '_session')
|
||||
assert 'scoped_session' in type(Trade._session).__name__
|
||||
|
||||
|
@ -36,7 +36,7 @@ def test_init_custom_db_url(default_conf, tmpdir):
|
|||
|
||||
default_conf.update({'db_url': f'sqlite:///{filename}'})
|
||||
|
||||
init_db(default_conf['db_url'], default_conf['dry_run'])
|
||||
init_db(default_conf['db_url'])
|
||||
assert Path(filename).is_file()
|
||||
r = Trade._session.execute(text("PRAGMA journal_mode"))
|
||||
assert r.first() == ('wal',)
|
||||
|
@ -45,10 +45,10 @@ def test_init_custom_db_url(default_conf, tmpdir):
|
|||
def test_init_invalid_db_url():
|
||||
# Update path to a value other than default, but still in-memory
|
||||
with pytest.raises(OperationalException, match=r'.*no valid database URL*'):
|
||||
init_db('unknown:///some.url', True)
|
||||
init_db('unknown:///some.url')
|
||||
|
||||
with pytest.raises(OperationalException, match=r'Bad db-url.*For in-memory database, pl.*'):
|
||||
init_db('sqlite:///', True)
|
||||
init_db('sqlite:///')
|
||||
|
||||
|
||||
def test_init_prod_db(default_conf, mocker):
|
||||
|
@ -57,7 +57,7 @@ def test_init_prod_db(default_conf, mocker):
|
|||
|
||||
create_engine_mock = mocker.patch('freqtrade.persistence.models.create_engine', MagicMock())
|
||||
|
||||
init_db(default_conf['db_url'], default_conf['dry_run'])
|
||||
init_db(default_conf['db_url'])
|
||||
assert create_engine_mock.call_count == 1
|
||||
assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tradesv3.sqlite'
|
||||
|
||||
|
@ -70,7 +70,7 @@ def test_init_dryrun_db(default_conf, tmpdir):
|
|||
'db_url': f'sqlite:///{filename}'
|
||||
})
|
||||
|
||||
init_db(default_conf['db_url'], default_conf['dry_run'])
|
||||
init_db(default_conf['db_url'])
|
||||
assert Path(filename).is_file()
|
||||
|
||||
|
||||
|
@ -1129,56 +1129,6 @@ def test_calc_profit(
|
|||
assert pytest.approx(trade.calc_profit_ratio(rate=close_rate)) == round(profit_ratio, 8)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_persistence")
|
||||
def test_clean_dry_run_db(default_conf, fee):
|
||||
|
||||
# Simulate dry_run entries
|
||||
trade = Trade(
|
||||
pair='ADA/USDT',
|
||||
stake_amount=0.001,
|
||||
amount=123.0,
|
||||
fee_open=fee.return_value,
|
||||
fee_close=fee.return_value,
|
||||
open_rate=0.123,
|
||||
exchange='binance',
|
||||
open_order_id='dry_run_buy_12345'
|
||||
)
|
||||
Trade.query.session.add(trade)
|
||||
|
||||
trade = Trade(
|
||||
pair='ETC/BTC',
|
||||
stake_amount=0.001,
|
||||
amount=123.0,
|
||||
fee_open=fee.return_value,
|
||||
fee_close=fee.return_value,
|
||||
open_rate=0.123,
|
||||
exchange='binance',
|
||||
open_order_id='dry_run_sell_12345'
|
||||
)
|
||||
Trade.query.session.add(trade)
|
||||
|
||||
# Simulate prod entry
|
||||
trade = Trade(
|
||||
pair='ETC/BTC',
|
||||
stake_amount=0.001,
|
||||
amount=123.0,
|
||||
fee_open=fee.return_value,
|
||||
fee_close=fee.return_value,
|
||||
open_rate=0.123,
|
||||
exchange='binance',
|
||||
open_order_id='prod_buy_12345'
|
||||
)
|
||||
Trade.query.session.add(trade)
|
||||
|
||||
# We have 3 entries: 2 dry_run, 1 prod
|
||||
assert len(Trade.query.filter(Trade.open_order_id.isnot(None)).all()) == 3
|
||||
|
||||
clean_dry_run_db()
|
||||
|
||||
# We have now only the prod
|
||||
assert len(Trade.query.filter(Trade.open_order_id.isnot(None)).all()) == 1
|
||||
|
||||
|
||||
def test_migrate_new(mocker, default_conf, fee, caplog):
|
||||
"""
|
||||
Test Database migration (starting with new pairformat)
|
||||
|
@ -1310,7 +1260,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
|
|||
|
||||
connection.execute(text("create table trades_bak1 as select * from trades"))
|
||||
# Run init to test migration
|
||||
init_db(default_conf['db_url'], default_conf['dry_run'])
|
||||
init_db(default_conf['db_url'])
|
||||
|
||||
assert len(Trade.query.filter(Trade.id == 1).all()) == 1
|
||||
trade = Trade.query.filter(Trade.id == 1).first()
|
||||
|
@ -1393,7 +1343,7 @@ def test_migrate_too_old(mocker, default_conf, fee, caplog):
|
|||
|
||||
# Run init to test migration
|
||||
with pytest.raises(OperationalException, match=r'Your database seems to be very old'):
|
||||
init_db(default_conf['db_url'], default_conf['dry_run'])
|
||||
init_db(default_conf['db_url'])
|
||||
|
||||
|
||||
def test_migrate_get_last_sequence_ids():
|
||||
|
@ -1467,7 +1417,7 @@ def test_migrate_pairlocks(mocker, default_conf, fee, caplog):
|
|||
connection.execute(text(create_index2))
|
||||
connection.execute(text(create_index3))
|
||||
|
||||
init_db(default_conf['db_url'], default_conf['dry_run'])
|
||||
init_db(default_conf['db_url'])
|
||||
|
||||
assert len(PairLock.query.all()) == 2
|
||||
assert len(PairLock.query.filter(PairLock.pair == '*').all()) == 1
|
||||
|
@ -2721,3 +2671,21 @@ def test_select_filled_orders(fee):
|
|||
orders = trades[4].select_filled_orders('sell')
|
||||
assert orders is not None
|
||||
assert len(orders) == 0
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_persistence")
|
||||
def test_order_to_ccxt(limit_buy_order_open):
|
||||
|
||||
order = Order.parse_from_ccxt_object(limit_buy_order_open, 'mocked', 'buy')
|
||||
order.query.session.add(order)
|
||||
Order.query.session.commit()
|
||||
|
||||
order_resp = Order.order_by_id(limit_buy_order_open['id'])
|
||||
assert order_resp
|
||||
|
||||
raw_order = order_resp.to_ccxt_object()
|
||||
del raw_order['fee']
|
||||
del raw_order['datetime']
|
||||
del raw_order['info']
|
||||
del limit_buy_order_open['datetime']
|
||||
assert raw_order == limit_buy_order_open
|
||||
|
|
Loading…
Reference in New Issue
Block a user