commit
2e397a88e1
22
.github/workflows/ci.yml
vendored
22
.github/workflows/ci.yml
vendored
@ -23,10 +23,10 @@ jobs:
|
|||||||
python-version: ["3.8", "3.9", "3.10"]
|
python-version: ["3.8", "3.9", "3.10"]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v3
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
@ -118,10 +118,10 @@ jobs:
|
|||||||
python-version: ["3.8", "3.9", "3.10"]
|
python-version: ["3.8", "3.9", "3.10"]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v3
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
@ -210,10 +210,10 @@ jobs:
|
|||||||
python-version: ["3.8", "3.9", "3.10"]
|
python-version: ["3.8", "3.9", "3.10"]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v3
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
@ -262,14 +262,14 @@ jobs:
|
|||||||
docs_check:
|
docs_check:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Documentation syntax
|
- name: Documentation syntax
|
||||||
run: |
|
run: |
|
||||||
./tests/test_docs.sh
|
./tests/test_docs.sh
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v3
|
||||||
with:
|
with:
|
||||||
python-version: 3.8
|
python-version: 3.8
|
||||||
|
|
||||||
@ -325,10 +325,10 @@ jobs:
|
|||||||
if: (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'release') && github.repository == 'freqtrade/freqtrade'
|
if: (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'release') && github.repository == 'freqtrade/freqtrade'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v3
|
||||||
with:
|
with:
|
||||||
python-version: 3.8
|
python-version: 3.8
|
||||||
|
|
||||||
@ -405,7 +405,7 @@ jobs:
|
|||||||
if: (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'release') && github.repository == 'freqtrade/freqtrade'
|
if: (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'release') && github.repository == 'freqtrade/freqtrade'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Extract branch name
|
- name: Extract branch name
|
||||||
shell: bash
|
shell: bash
|
||||||
|
2
.github/workflows/docker_update_readme.yml
vendored
2
.github/workflows/docker_update_readme.yml
vendored
@ -8,7 +8,7 @@ jobs:
|
|||||||
dockerHubDescription:
|
dockerHubDescription:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v3
|
||||||
- name: Docker Hub Description
|
- name: Docker Hub Description
|
||||||
uses: peter-evans/dockerhub-description@v2.4.3
|
uses: peter-evans/dockerhub-description@v2.4.3
|
||||||
env:
|
env:
|
||||||
|
17
README.md
17
README.md
@ -30,12 +30,13 @@ hesitate to read the source code and understand the mechanism of this bot.
|
|||||||
|
|
||||||
Please read the [exchange specific notes](docs/exchanges.md) to learn about eventual, special configurations needed for each exchange.
|
Please read the [exchange specific notes](docs/exchanges.md) to learn about eventual, special configurations needed for each exchange.
|
||||||
|
|
||||||
- [X] [Binance](https://www.binance.com/) ([*Note for binance users](docs/exchanges.md#binance-blacklist))
|
- [X] [Binance](https://www.binance.com/)
|
||||||
- [X] [Bittrex](https://bittrex.com/)
|
- [X] [Bittrex](https://bittrex.com/)
|
||||||
- [X] [FTX](https://ftx.com)
|
- [X] [FTX](https://ftx.com/#a=2258149)
|
||||||
- [X] [Gate.io](https://www.gate.io/ref/6266643)
|
- [X] [Gate.io](https://www.gate.io/ref/6266643)
|
||||||
|
- [X] [Huobi](http://huobi.com/)
|
||||||
- [X] [Kraken](https://kraken.com/)
|
- [X] [Kraken](https://kraken.com/)
|
||||||
- [X] [OKX](https://www.okx.com/)
|
- [X] [OKX](https://okx.com/) (Former OKEX)
|
||||||
- [ ] [potentially many others](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
|
- [ ] [potentially many others](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
|
||||||
|
|
||||||
### Community tested
|
### Community tested
|
||||||
@ -68,15 +69,9 @@ Please find the complete documentation on the [freqtrade website](https://www.fr
|
|||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
|
|
||||||
Freqtrade provides a Linux/macOS script to install all dependencies and help you to configure the bot.
|
Please refer to the [Docker Quickstart documentation](https://www.freqtrade.io/en/stable/docker_quickstart/) on how to get started quickly.
|
||||||
|
|
||||||
```bash
|
For further (native) installation methods, please refer to the [Installation documentation page](https://www.freqtrade.io/en/stable/installation/).
|
||||||
git clone -b develop https://github.com/freqtrade/freqtrade.git
|
|
||||||
cd freqtrade
|
|
||||||
./setup.sh --install
|
|
||||||
```
|
|
||||||
|
|
||||||
For any other type of installation please refer to [Installation doc](https://www.freqtrade.io/en/stable/installation/).
|
|
||||||
|
|
||||||
## Basic Usage
|
## Basic Usage
|
||||||
|
|
||||||
|
@ -56,7 +56,8 @@
|
|||||||
"forcebuy": "market",
|
"forcebuy": "market",
|
||||||
"stoploss": "market",
|
"stoploss": "market",
|
||||||
"stoploss_on_exchange": false,
|
"stoploss_on_exchange": false,
|
||||||
"stoploss_on_exchange_interval": 60
|
"stoploss_on_exchange_interval": 60,
|
||||||
|
"stoploss_on_exchange_limit_ratio": 0.99
|
||||||
},
|
},
|
||||||
"order_time_in_force": {
|
"order_time_in_force": {
|
||||||
"buy": "gtc",
|
"buy": "gtc",
|
||||||
|
@ -26,7 +26,7 @@ usage: freqtrade backtesting [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
|||||||
|
|
||||||
optional arguments:
|
optional arguments:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
-i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME
|
-i TIMEFRAME, --timeframe TIMEFRAME
|
||||||
Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`).
|
Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`).
|
||||||
--timerange TIMERANGE
|
--timerange TIMERANGE
|
||||||
Specify what timerange of data to use.
|
Specify what timerange of data to use.
|
||||||
@ -63,7 +63,7 @@ optional arguments:
|
|||||||
`30m`, `1h`, `1d`).
|
`30m`, `1h`, `1d`).
|
||||||
--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]
|
--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]
|
||||||
Provide a space-separated list of strategies to
|
Provide a space-separated list of strategies to
|
||||||
backtest. Please note that ticker-interval needs to be
|
backtest. Please note that timeframe needs to be
|
||||||
set either in config or via command line. When using
|
set either in config or via command line. When using
|
||||||
this together with `--export trades`, the strategy-
|
this together with `--export trades`, the strategy-
|
||||||
name is injected into the filename (so `backtest-
|
name is injected into the filename (so `backtest-
|
||||||
|
@ -24,7 +24,7 @@ By default, loop runs every few seconds (`internals.process_throttle_secs`) and
|
|||||||
|
|
||||||
* Fetch open trades from persistence.
|
* Fetch open trades from persistence.
|
||||||
* Calculate current list of tradable pairs.
|
* Calculate current list of tradable pairs.
|
||||||
* Download ohlcv data for the pairlist including all [informative pairs](strategy-customization.md#get-data-for-non-tradeable-pairs)
|
* Download OHLCV data for the pairlist including all [informative pairs](strategy-customization.md#get-data-for-non-tradeable-pairs)
|
||||||
This step is only executed once per Candle to avoid unnecessary network traffic.
|
This step is only executed once per Candle to avoid unnecessary network traffic.
|
||||||
* Call `bot_loop_start()` strategy callback.
|
* Call `bot_loop_start()` strategy callback.
|
||||||
* Analyze strategy per pair.
|
* Analyze strategy per pair.
|
||||||
|
@ -86,7 +86,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
|||||||
| `amend_last_stake_amount` | Use reduced last stake amount if necessary. [More information below](#configuring-amount-per-trade). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
| `amend_last_stake_amount` | Use reduced last stake amount if necessary. [More information below](#configuring-amount-per-trade). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
||||||
| `last_stake_amount_min_ratio` | Defines minimum stake amount that has to be left and executed. Applies only to the last stake amount when it's amended to a reduced value (i.e. if `amend_last_stake_amount` is set to `true`). [More information below](#configuring-amount-per-trade). <br>*Defaults to `0.5`.* <br> **Datatype:** Float (as ratio)
|
| `last_stake_amount_min_ratio` | Defines minimum stake amount that has to be left and executed. Applies only to the last stake amount when it's amended to a reduced value (i.e. if `amend_last_stake_amount` is set to `true`). [More information below](#configuring-amount-per-trade). <br>*Defaults to `0.5`.* <br> **Datatype:** Float (as ratio)
|
||||||
| `amount_reserve_percent` | Reserve some amount in min pair stake amount. The bot will reserve `amount_reserve_percent` + stoploss value when calculating min pair stake amount in order to avoid possible trade refusals. <br>*Defaults to `0.05` (5%).* <br> **Datatype:** Positive Float as ratio.
|
| `amount_reserve_percent` | Reserve some amount in min pair stake amount. The bot will reserve `amount_reserve_percent` + stoploss value when calculating min pair stake amount in order to avoid possible trade refusals. <br>*Defaults to `0.05` (5%).* <br> **Datatype:** Positive Float as ratio.
|
||||||
| `timeframe` | The timeframe (former ticker interval) to use (e.g `1m`, `5m`, `15m`, `30m`, `1h` ...). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** String
|
| `timeframe` | The timeframe to use (e.g `1m`, `5m`, `15m`, `30m`, `1h` ...). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** String
|
||||||
| `fiat_display_currency` | Fiat currency used to show your profits. [More information below](#what-values-can-be-used-for-fiat_display_currency). <br> **Datatype:** String
|
| `fiat_display_currency` | Fiat currency used to show your profits. [More information below](#what-values-can-be-used-for-fiat_display_currency). <br> **Datatype:** String
|
||||||
| `dry_run` | **Required.** Define if the bot must be in Dry Run or production mode. <br>*Defaults to `true`.* <br> **Datatype:** Boolean
|
| `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
|
| `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
|
||||||
|
@ -24,6 +24,10 @@ Please refer to [pairlists](plugins.md#pairlists-and-pairlist-handlers) instead.
|
|||||||
Did only download the latest 500 candles, so was ineffective in getting good backtest data.
|
Did only download the latest 500 candles, so was ineffective in getting good backtest data.
|
||||||
Removed in 2019-7-dev (develop branch) and in freqtrade 2019.8.
|
Removed in 2019-7-dev (develop branch) and in freqtrade 2019.8.
|
||||||
|
|
||||||
|
### `ticker_interval` (now `timeframe`)
|
||||||
|
|
||||||
|
Support for `ticker_interval` terminology was deprecated in 2020.6 in favor of `timeframe` - and compatibility code was removed in 2022.3.
|
||||||
|
|
||||||
### Allow running multiple pairlists in sequence
|
### Allow running multiple pairlists in sequence
|
||||||
|
|
||||||
The former `"pairlist"` section in the configuration has been removed, and is replaced by `"pairlists"` - being a list to specify a sequence of pairlists.
|
The former `"pairlist"` section in the configuration has been removed, and is replaced by `"pairlists"` - being a list to specify a sequence of pairlists.
|
||||||
|
@ -222,7 +222,7 @@ usage: freqtrade edge [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
|
|||||||
|
|
||||||
optional arguments:
|
optional arguments:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
-i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME
|
-i TIMEFRAME, --timeframe TIMEFRAME
|
||||||
Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`).
|
Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`).
|
||||||
--timerange TIMERANGE
|
--timerange TIMERANGE
|
||||||
Specify what timerange of data to use.
|
Specify what timerange of data to use.
|
||||||
|
@ -57,7 +57,7 @@ This configuration enables kraken, as well as rate-limiting to avoid bans from t
|
|||||||
Binance supports [time_in_force](configuration.md#understand-order_time_in_force).
|
Binance supports [time_in_force](configuration.md#understand-order_time_in_force).
|
||||||
|
|
||||||
!!! Tip "Stoploss on Exchange"
|
!!! Tip "Stoploss on Exchange"
|
||||||
Binance supports `stoploss_on_exchange` and uses stop-loss-limit orders. It provides great advantages, so we recommend to benefit from it.
|
Binance supports `stoploss_on_exchange` and uses `stop-loss-limit` orders. It provides great advantages, so we recommend to benefit from it by enabling stoploss on exchange..
|
||||||
|
|
||||||
### Binance Blacklist
|
### Binance Blacklist
|
||||||
|
|
||||||
@ -177,12 +177,21 @@ Kucoin requires a passphrase for each api key, you will therefore need to add th
|
|||||||
|
|
||||||
Kucoin supports [time_in_force](configuration.md#understand-order_time_in_force).
|
Kucoin supports [time_in_force](configuration.md#understand-order_time_in_force).
|
||||||
|
|
||||||
|
!!! Tip "Stoploss on Exchange"
|
||||||
|
Kucoin supports `stoploss_on_exchange` and can use both stop-loss-market and stop-loss-limit orders. It provides great advantages, so we recommend to benefit from it.
|
||||||
|
You can use either `"limit"` or `"market"` in the `order_types.stoploss` configuration setting to decide which type of stoploss shall be used.
|
||||||
|
|
||||||
### Kucoin Blacklists
|
### Kucoin Blacklists
|
||||||
|
|
||||||
For Kucoin, please add `"KCS/<STAKE>"` to your blacklist to avoid issues.
|
For Kucoin, please add `"KCS/<STAKE>"` to your blacklist to avoid issues.
|
||||||
Accounts having KCS accounts use this to pay for fees - if your first trade happens to be on `KCS`, further trades will consume this position and make the initial KCS trade unsellable as the expected amount is not there anymore.
|
Accounts having KCS accounts use this to pay for fees - if your first trade happens to be on `KCS`, further trades will consume this position and make the initial KCS trade unsellable as the expected amount is not there anymore.
|
||||||
|
|
||||||
## OKX
|
## Huobi
|
||||||
|
|
||||||
|
!!! Tip "Stoploss on Exchange"
|
||||||
|
Huobi supports `stoploss_on_exchange` and uses `stop-limit` orders. It provides great advantages, so we recommend to benefit from it by enabling stoploss on exchange.
|
||||||
|
|
||||||
|
## OKX (former OKEX)
|
||||||
|
|
||||||
OKX requires a passphrase for each api key, you will therefore need to add this key into the configuration so your exchange section looks as follows:
|
OKX requires a passphrase for each api key, you will therefore need to add this key into the configuration so your exchange section looks as follows:
|
||||||
|
|
||||||
@ -201,6 +210,9 @@ OKX requires a passphrase for each api key, you will therefore need to add this
|
|||||||
|
|
||||||
## Gate.io
|
## Gate.io
|
||||||
|
|
||||||
|
!!! Tip "Stoploss on Exchange"
|
||||||
|
Gate.io supports `stoploss_on_exchange` and uses `stop-loss-limit` orders. It provides great advantages, so we recommend to benefit from it by enabling stoploss on exchange..
|
||||||
|
|
||||||
Gate.io allows the use of `POINT` to pay for fees. As this is not a tradable currency (no regular market available), automatic fee calculations will fail (and default to a fee of 0).
|
Gate.io allows the use of `POINT` to pay for fees. As this is not a tradable currency (no regular market available), automatic fee calculations will fail (and default to a fee of 0).
|
||||||
The configuration parameter `exchange.unknown_fee_rate` can be used to specify the exchange rate between Point and the stake currency. Obviously, changing the stake-currency will also require changes to this value.
|
The configuration parameter `exchange.unknown_fee_rate` can be used to specify the exchange rate between Point and the stake currency. Obviously, changing the stake-currency will also require changes to this value.
|
||||||
|
|
||||||
|
@ -55,7 +55,7 @@ usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
|
|||||||
|
|
||||||
optional arguments:
|
optional arguments:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
-i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME
|
-i TIMEFRAME, --timeframe TIMEFRAME
|
||||||
Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`).
|
Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`).
|
||||||
--timerange TIMERANGE
|
--timerange TIMERANGE
|
||||||
Specify what timerange of data to use.
|
Specify what timerange of data to use.
|
||||||
|
@ -51,9 +51,9 @@ When buying with the orderbook enabled (`bid_strategy.use_order_book=True`), Fre
|
|||||||
|
|
||||||
#### Buy price without Orderbook enabled
|
#### Buy price without Orderbook enabled
|
||||||
|
|
||||||
The following section uses `side` as the configured `bid_strategy.price_side`.
|
The following section uses `side` as the configured `bid_strategy.price_side` (defaults to `"bid"`).
|
||||||
|
|
||||||
When not using orderbook (`bid_strategy.use_order_book=False`), Freqtrade uses the best `side` price from the ticker if it's below the `last` traded price from the ticker. Otherwise (when the `side` price is above the `last` price), it calculates a rate between `side` and `last` price.
|
When not using orderbook (`bid_strategy.use_order_book=False`), Freqtrade uses the best `side` price from the ticker if it's below the `last` traded price from the ticker. Otherwise (when the `side` price is above the `last` price), it calculates a rate between `side` and `last` price based on `bid_strategy.ask_last_balance`..
|
||||||
|
|
||||||
The `bid_strategy.ask_last_balance` configuration parameter controls this. A value of `0.0` will use `side` price, while `1.0` will use the `last` price and values between those interpolate between ask and last price.
|
The `bid_strategy.ask_last_balance` configuration parameter controls this. A value of `0.0` will use `side` price, while `1.0` will use the `last` price and values between those interpolate between ask and last price.
|
||||||
|
|
||||||
@ -88,9 +88,9 @@ When selling with the orderbook enabled (`ask_strategy.use_order_book=True`), Fr
|
|||||||
|
|
||||||
#### Sell price without Orderbook enabled
|
#### Sell price without Orderbook enabled
|
||||||
|
|
||||||
When not using orderbook (`ask_strategy.use_order_book=False`), the price at the `ask_strategy.price_side` side (defaults to `"ask"`) from the ticker will be used as the sell price.
|
The following section uses `side` as the configured `ask_strategy.price_side` (defaults to `"ask"`).
|
||||||
|
|
||||||
When not using orderbook (`ask_strategy.use_order_book=False`), Freqtrade uses the best `side` price from the ticker if it's below the `last` traded price from the ticker. Otherwise (when the `side` price is above the `last` price), it calculates a rate between `side` and `last` price.
|
When not using orderbook (`ask_strategy.use_order_book=False`), Freqtrade uses the best `side` price from the ticker if it's above the `last` traded price from the ticker. Otherwise (when the `side` price is below the `last` price), it calculates a rate between `side` and `last` price based on `ask_strategy.bid_last_balance`.
|
||||||
|
|
||||||
The `ask_strategy.bid_last_balance` configuration parameter controls this. A value of `0.0` will use `side` price, while `1.0` will use the last price and values between those interpolate between `side` and last price.
|
The `ask_strategy.bid_last_balance` configuration parameter controls this. A value of `0.0` will use `side` price, while `1.0` will use the last price and values between those interpolate between `side` and last price.
|
||||||
|
|
||||||
|
@ -42,12 +42,13 @@ Freqtrade is a free and open source crypto trading bot written in Python. It is
|
|||||||
|
|
||||||
Please read the [exchange specific notes](exchanges.md) to learn about eventual, special configurations needed for each exchange.
|
Please read the [exchange specific notes](exchanges.md) to learn about eventual, special configurations needed for each exchange.
|
||||||
|
|
||||||
- [X] [Binance](https://www.binance.com/) ([*Note for binance users](exchanges.md#binance-blacklist))
|
- [X] [Binance](https://www.binance.com/)
|
||||||
- [X] [Bittrex](https://bittrex.com/)
|
- [X] [Bittrex](https://bittrex.com/)
|
||||||
- [X] [FTX](https://ftx.com)
|
- [X] [FTX](https://ftx.com/#a=2258149)
|
||||||
- [X] [Gate.io](https://www.gate.io/ref/6266643)
|
- [X] [Gate.io](https://www.gate.io/ref/6266643)
|
||||||
|
- [X] [Huobi](http://huobi.com/)
|
||||||
- [X] [Kraken](https://kraken.com/)
|
- [X] [Kraken](https://kraken.com/)
|
||||||
- [X] [OKX](https://www.okx.com/)
|
- [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)_
|
- [ ] [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)_
|
||||||
|
|
||||||
### Community tested
|
### Community tested
|
||||||
|
@ -65,7 +65,7 @@ optional arguments:
|
|||||||
_today.json`
|
_today.json`
|
||||||
--timerange TIMERANGE
|
--timerange TIMERANGE
|
||||||
Specify what timerange of data to use.
|
Specify what timerange of data to use.
|
||||||
-i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME
|
-i TIMEFRAME, --timeframe TIMEFRAME
|
||||||
Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`).
|
Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`).
|
||||||
--no-trades Skip using trades from backtesting file and DB.
|
--no-trades Skip using trades from backtesting file and DB.
|
||||||
|
|
||||||
@ -330,7 +330,7 @@ optional arguments:
|
|||||||
--trade-source {DB,file}
|
--trade-source {DB,file}
|
||||||
Specify the source for trades (Can be DB or file
|
Specify the source for trades (Can be DB or file
|
||||||
(backtest file)) Default: file
|
(backtest file)) Default: file
|
||||||
-i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME
|
-i TIMEFRAME, --timeframe TIMEFRAME
|
||||||
Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`).
|
Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`).
|
||||||
--auto-open Automatically open generated plot.
|
--auto-open Automatically open generated plot.
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
mkdocs==1.2.3
|
mkdocs==1.2.3
|
||||||
mkdocs-material==8.2.1
|
mkdocs-material==8.2.5
|
||||||
mdx_truly_sane_lists==1.2
|
mdx_truly_sane_lists==1.2
|
||||||
pymdown-extensions==9.2
|
pymdown-extensions==9.3
|
||||||
|
jinja2==3.0.3
|
||||||
|
@ -24,7 +24,7 @@ These modes can be configured with these values:
|
|||||||
```
|
```
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
Stoploss on exchange is only supported for Binance (stop-loss-limit), Kraken (stop-loss-market, stop-loss-limit) and FTX (stop limit and stop-market) as of now.
|
Stoploss on exchange is only supported for Binance (stop-loss-limit), Huobi (stop-limit), Kraken (stop-loss-market, stop-loss-limit), FTX (stop limit and stop-market) Gateio (stop-limit), and Kucoin (stop-limit and stop-market) as of now.
|
||||||
<ins>Do not set too low/tight stoploss value if using stop loss on exchange!</ins>
|
<ins>Do not set too low/tight stoploss value if using stop loss on exchange!</ins>
|
||||||
If set to low/tight then you have greater risk of missing fill on the order and stoploss will not work.
|
If set to low/tight then you have greater risk of missing fill on the order and stoploss will not work.
|
||||||
|
|
||||||
|
@ -146,7 +146,7 @@ def version(self) -> str:
|
|||||||
|
|
||||||
The strategies can be derived from other strategies. This avoids duplication of your custom strategy code. You can use this technique to override small parts of your main strategy, leaving the rest untouched:
|
The strategies can be derived from other strategies. This avoids duplication of your custom strategy code. You can use this technique to override small parts of your main strategy, leaving the rest untouched:
|
||||||
|
|
||||||
``` python
|
``` python title="user_data/strategies/myawesomestrategy.py"
|
||||||
class MyAwesomeStrategy(IStrategy):
|
class MyAwesomeStrategy(IStrategy):
|
||||||
...
|
...
|
||||||
stoploss = 0.13
|
stoploss = 0.13
|
||||||
@ -155,6 +155,10 @@ class MyAwesomeStrategy(IStrategy):
|
|||||||
# should be in any custom strategy...
|
# should be in any custom strategy...
|
||||||
...
|
...
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
``` python title="user_data/strategies/MyAwesomeStrategy2.py"
|
||||||
|
from myawesomestrategy import MyAwesomeStrategy
|
||||||
class MyAwesomeStrategy2(MyAwesomeStrategy):
|
class MyAwesomeStrategy2(MyAwesomeStrategy):
|
||||||
# Override something
|
# Override something
|
||||||
stoploss = 0.08
|
stoploss = 0.08
|
||||||
@ -163,16 +167,7 @@ class MyAwesomeStrategy2(MyAwesomeStrategy):
|
|||||||
|
|
||||||
Both attributes and methods may be overridden, altering behavior of the original strategy in a way you need.
|
Both attributes and methods may be overridden, altering behavior of the original strategy in a way you need.
|
||||||
|
|
||||||
!!! Note "Parent-strategy in different files"
|
While keeping the subclass in the same file is technically possible, it can lead to some problems with hyperopt parameter files, we therefore recommend to use separate strategy files, and import the parent strategy as shown above.
|
||||||
If you have the parent-strategy in a different file, you'll need to add the following to the top of your "child"-file to ensure proper loading, otherwise freqtrade may not be able to load the parent strategy correctly.
|
|
||||||
|
|
||||||
``` python
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
sys.path.append(str(Path(__file__).parent))
|
|
||||||
|
|
||||||
from myawesomestrategy import MyAwesomeStrategy
|
|
||||||
```
|
|
||||||
|
|
||||||
## Embedding Strategies
|
## Embedding Strategies
|
||||||
|
|
||||||
|
@ -325,7 +325,7 @@ stoploss = -0.10
|
|||||||
|
|
||||||
For the full documentation on stoploss features, look at the dedicated [stoploss page](stoploss.md).
|
For the full documentation on stoploss features, look at the dedicated [stoploss page](stoploss.md).
|
||||||
|
|
||||||
### Timeframe (formerly ticker interval)
|
### Timeframe
|
||||||
|
|
||||||
This is the set of candles the bot should download and use for the analysis.
|
This is the set of candles the bot should download and use for the analysis.
|
||||||
Common values are `"1m"`, `"5m"`, `"15m"`, `"1h"`, however all values supported by your exchange should work.
|
Common values are `"1m"`, `"5m"`, `"15m"`, `"1h"`, however all values supported by your exchange should work.
|
||||||
|
@ -277,6 +277,7 @@ Starting capital is either taken from the `available_capital` setting, or calcul
|
|||||||
> **BITTREX:** Buying ETH/BTC with limit `0.03400000` (`1.000000 ETH`, `225.290 USD`)
|
> **BITTREX:** Buying ETH/BTC with limit `0.03400000` (`1.000000 ETH`, `225.290 USD`)
|
||||||
|
|
||||||
Omitting the pair will open a query asking for the pair to buy (based on the current whitelist).
|
Omitting the pair will open a query asking for the pair to buy (based on the current whitelist).
|
||||||
|
Trades crated through `/forcebuy` will have the buy-tag of `forceentry`.
|
||||||
|
|
||||||
![Telegram force-buy screenshot](assets/telegram_forcebuy.png)
|
![Telegram force-buy screenshot](assets/telegram_forcebuy.png)
|
||||||
|
|
||||||
|
@ -517,20 +517,25 @@ Requires a configuration with specified `pairlists` attribute.
|
|||||||
Can be used to generate static pairlists to be used during backtesting / hyperopt.
|
Can be used to generate static pairlists to be used during backtesting / hyperopt.
|
||||||
|
|
||||||
```
|
```
|
||||||
usage: freqtrade test-pairlist [-h] [-c PATH]
|
usage: freqtrade test-pairlist [-h] [-v] [-c PATH]
|
||||||
[--quote QUOTE_CURRENCY [QUOTE_CURRENCY ...]]
|
[--quote QUOTE_CURRENCY [QUOTE_CURRENCY ...]]
|
||||||
[-1] [--print-json]
|
[-1] [--print-json] [--exchange EXCHANGE]
|
||||||
|
|
||||||
optional arguments:
|
optional arguments:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
|
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||||
-c PATH, --config PATH
|
-c PATH, --config PATH
|
||||||
Specify configuration file (default: `config.json`).
|
Specify configuration file (default:
|
||||||
Multiple --config options may be used. Can be set to
|
`userdir/config.json` or `config.json` whichever
|
||||||
`-` to read config from stdin.
|
exists). Multiple --config options may be used. Can be
|
||||||
|
set to `-` to read config from stdin.
|
||||||
--quote QUOTE_CURRENCY [QUOTE_CURRENCY ...]
|
--quote QUOTE_CURRENCY [QUOTE_CURRENCY ...]
|
||||||
Specify quote currency(-ies). Space-separated list.
|
Specify quote currency(-ies). Space-separated list.
|
||||||
-1, --one-column Print output in one column.
|
-1, --one-column Print output in one column.
|
||||||
--print-json Print list of pairs or market symbols in JSON format.
|
--print-json Print list of pairs or market symbols in JSON format.
|
||||||
|
--exchange EXCHANGE Exchange name (default: `bittrex`). Only valid if no
|
||||||
|
config is provided.
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Examples
|
### Examples
|
||||||
|
@ -1,27 +1,14 @@
|
|||||||
""" Freqtrade bot """
|
""" Freqtrade bot """
|
||||||
__version__ = '2022.2.2'
|
__version__ = '2022.3'
|
||||||
|
|
||||||
if __version__ == 'develop':
|
|
||||||
|
|
||||||
|
if 'dev' in __version__:
|
||||||
try:
|
try:
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
__version__ = 'develop-' + subprocess.check_output(
|
__version__ = __version__ + '-' + subprocess.check_output(
|
||||||
['git', 'log', '--format="%h"', '-n 1'],
|
['git', 'log', '--format="%h"', '-n 1'],
|
||||||
stderr=subprocess.DEVNULL).decode("utf-8").rstrip().strip('"')
|
stderr=subprocess.DEVNULL).decode("utf-8").rstrip().strip('"')
|
||||||
|
|
||||||
# from datetime import datetime
|
|
||||||
# last_release = subprocess.check_output(
|
|
||||||
# ['git', 'tag']
|
|
||||||
# ).decode('utf-8').split()[-1].split(".")
|
|
||||||
# # Releases are in the format "2020.1" - we increment the latest version for dev.
|
|
||||||
# prefix = f"{last_release[0]}.{int(last_release[1]) + 1}"
|
|
||||||
# dev_version = int(datetime.now().timestamp() // 1000)
|
|
||||||
# __version__ = f"{prefix}.dev{dev_version}"
|
|
||||||
|
|
||||||
# subprocess.check_output(
|
|
||||||
# ['git', 'log', '--format="%h"', '-n 1'],
|
|
||||||
# stderr=subprocess.DEVNULL).decode("utf-8").rstrip().strip('"')
|
|
||||||
except Exception: # pragma: no cover
|
except Exception: # pragma: no cover
|
||||||
# git not available, ignore
|
# git not available, ignore
|
||||||
try:
|
try:
|
||||||
|
@ -51,7 +51,7 @@ ARGS_LIST_PAIRS = ["exchange", "print_list", "list_pairs_print_json", "print_one
|
|||||||
"print_csv", "base_currencies", "quote_currencies", "list_pairs_all"]
|
"print_csv", "base_currencies", "quote_currencies", "list_pairs_all"]
|
||||||
|
|
||||||
ARGS_TEST_PAIRLIST = ["verbosity", "config", "quote_currencies", "print_one_column",
|
ARGS_TEST_PAIRLIST = ["verbosity", "config", "quote_currencies", "print_one_column",
|
||||||
"list_pairs_print_json"]
|
"list_pairs_print_json", "exchange"]
|
||||||
|
|
||||||
ARGS_CREATE_USERDIR = ["user_data_dir", "reset"]
|
ARGS_CREATE_USERDIR = ["user_data_dir", "reset"]
|
||||||
|
|
||||||
|
@ -108,10 +108,11 @@ def ask_user_config() -> Dict[str, Any]:
|
|||||||
"binance",
|
"binance",
|
||||||
"binanceus",
|
"binanceus",
|
||||||
"bittrex",
|
"bittrex",
|
||||||
"kraken",
|
|
||||||
"ftx",
|
"ftx",
|
||||||
"kucoin",
|
|
||||||
"gateio",
|
"gateio",
|
||||||
|
"huobi",
|
||||||
|
"kraken",
|
||||||
|
"kucoin",
|
||||||
"okx",
|
"okx",
|
||||||
Separator(),
|
Separator(),
|
||||||
"other",
|
"other",
|
||||||
|
@ -117,7 +117,7 @@ AVAILABLE_CLI_OPTIONS = {
|
|||||||
),
|
),
|
||||||
# Optimize common
|
# Optimize common
|
||||||
"timeframe": Arg(
|
"timeframe": Arg(
|
||||||
'-i', '--timeframe', '--ticker-interval',
|
'-i', '--timeframe',
|
||||||
help='Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`).',
|
help='Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`).',
|
||||||
),
|
),
|
||||||
"timerange": Arg(
|
"timerange": Arg(
|
||||||
@ -169,7 +169,7 @@ AVAILABLE_CLI_OPTIONS = {
|
|||||||
"strategy_list": Arg(
|
"strategy_list": Arg(
|
||||||
'--strategy-list',
|
'--strategy-list',
|
||||||
help='Provide a space-separated list of strategies to backtest. '
|
help='Provide a space-separated list of strategies to backtest. '
|
||||||
'Please note that ticker-interval needs to be set either in config '
|
'Please note that timeframe needs to be set either in config '
|
||||||
'or via command line. When using this together with `--export trades`, '
|
'or via command line. When using this together with `--export trades`, '
|
||||||
'the strategy-name is injected into the filename '
|
'the strategy-name is injected into the filename '
|
||||||
'(so `backtest-data.json` becomes `backtest-data-SampleStrategy.json`',
|
'(so `backtest-data.json` becomes `backtest-data-SampleStrategy.json`',
|
||||||
|
@ -25,12 +25,16 @@ def setup_optimize_configuration(args: Dict[str, Any], method: RunMode) -> Dict[
|
|||||||
RunMode.HYPEROPT: 'hyperoptimization',
|
RunMode.HYPEROPT: 'hyperoptimization',
|
||||||
}
|
}
|
||||||
if method in no_unlimited_runmodes.keys():
|
if method in no_unlimited_runmodes.keys():
|
||||||
|
wallet_size = config['dry_run_wallet'] * config['tradable_balance_ratio']
|
||||||
|
# tradable_balance_ratio
|
||||||
if (config['stake_amount'] != constants.UNLIMITED_STAKE_AMOUNT
|
if (config['stake_amount'] != constants.UNLIMITED_STAKE_AMOUNT
|
||||||
and config['stake_amount'] > config['dry_run_wallet']):
|
and config['stake_amount'] > wallet_size):
|
||||||
wallet = round_coin_value(config['dry_run_wallet'], config['stake_currency'])
|
wallet = round_coin_value(wallet_size, config['stake_currency'])
|
||||||
stake = round_coin_value(config['stake_amount'], config['stake_currency'])
|
stake = round_coin_value(config['stake_amount'], config['stake_currency'])
|
||||||
raise OperationalException(f"Starting balance ({wallet}) "
|
raise OperationalException(
|
||||||
f"is smaller than stake_amount {stake}.")
|
f"Starting balance ({wallet}) is smaller than stake_amount {stake}. "
|
||||||
|
f"Wallet is calculated as `dry_run_wallet * tradable_balance_ratio`."
|
||||||
|
)
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
@ -100,16 +100,11 @@ def process_temporary_deprecated_settings(config: Dict[str, Any]) -> None:
|
|||||||
"from the edge configuration."
|
"from the edge configuration."
|
||||||
)
|
)
|
||||||
if 'ticker_interval' in config:
|
if 'ticker_interval' in config:
|
||||||
logger.warning(
|
|
||||||
"DEPRECATED: "
|
raise OperationalException(
|
||||||
|
"DEPRECATED: 'ticker_interval' detected. "
|
||||||
"Please use 'timeframe' instead of 'ticker_interval."
|
"Please use 'timeframe' instead of 'ticker_interval."
|
||||||
)
|
)
|
||||||
if 'timeframe' in config:
|
|
||||||
raise OperationalException(
|
|
||||||
"Both 'timeframe' and 'ticker_interval' detected."
|
|
||||||
"Please remove 'ticker_interval' from your configuration to continue operating."
|
|
||||||
)
|
|
||||||
config['timeframe'] = config['ticker_interval']
|
|
||||||
|
|
||||||
if 'protections' in config:
|
if 'protections' in config:
|
||||||
logger.warning("DEPRECATED: Setting 'protections' in the configuration is deprecated.")
|
logger.warning("DEPRECATED: Setting 'protections' in the configuration is deprecated.")
|
||||||
|
@ -140,7 +140,7 @@ CONF_SCHEMA = {
|
|||||||
'minProperties': 1
|
'minProperties': 1
|
||||||
},
|
},
|
||||||
'amount_reserve_percent': {'type': 'number', 'minimum': 0.0, 'maximum': 0.5},
|
'amount_reserve_percent': {'type': 'number', 'minimum': 0.0, 'maximum': 0.5},
|
||||||
'stoploss': {'type': 'number', 'maximum': 0, 'exclusiveMaximum': True},
|
'stoploss': {'type': 'number', 'maximum': 0, 'exclusiveMaximum': True, 'minimum': -1},
|
||||||
'trailing_stop': {'type': 'boolean'},
|
'trailing_stop': {'type': 'boolean'},
|
||||||
'trailing_stop_positive': {'type': 'number', 'minimum': 0, 'maximum': 1},
|
'trailing_stop_positive': {'type': 'number', 'minimum': 0, 'maximum': 1},
|
||||||
'trailing_stop_positive_offset': {'type': 'number', 'minimum': 0, 'maximum': 1},
|
'trailing_stop_positive_offset': {'type': 'number', 'minimum': 0, 'maximum': 1},
|
||||||
@ -440,7 +440,6 @@ SCHEMA_TRADE_REQUIRED = [
|
|||||||
'dry_run_wallet',
|
'dry_run_wallet',
|
||||||
'ask_strategy',
|
'ask_strategy',
|
||||||
'bid_strategy',
|
'bid_strategy',
|
||||||
'unfilledtimeout',
|
|
||||||
'stoploss',
|
'stoploss',
|
||||||
'minimal_roi',
|
'minimal_roi',
|
||||||
'internals',
|
'internals',
|
||||||
@ -456,7 +455,6 @@ SCHEMA_BACKTEST_REQUIRED = [
|
|||||||
'dry_run_wallet',
|
'dry_run_wallet',
|
||||||
'dataformat_ohlcv',
|
'dataformat_ohlcv',
|
||||||
'dataformat_trades',
|
'dataformat_trades',
|
||||||
'unfilledtimeout',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
SCHEMA_MINIMAL_REQUIRED = [
|
SCHEMA_MINIMAL_REQUIRED = [
|
||||||
|
@ -219,9 +219,11 @@ class Edge:
|
|||||||
"""
|
"""
|
||||||
final = []
|
final = []
|
||||||
for pair, info in self._cached_pairs.items():
|
for pair, info in self._cached_pairs.items():
|
||||||
if info.expectancy > float(self.edge_config.get('minimum_expectancy', 0.2)) and \
|
if (
|
||||||
info.winrate > float(self.edge_config.get('minimum_winrate', 0.60)) and \
|
info.expectancy > float(self.edge_config.get('minimum_expectancy', 0.2))
|
||||||
pair in pairs:
|
and info.winrate > float(self.edge_config.get('minimum_winrate', 0.60))
|
||||||
|
and pair in pairs
|
||||||
|
):
|
||||||
final.append(pair)
|
final.append(pair)
|
||||||
|
|
||||||
if self._final_pairs != final:
|
if self._final_pairs != final:
|
||||||
@ -246,8 +248,8 @@ class Edge:
|
|||||||
"""
|
"""
|
||||||
final = []
|
final = []
|
||||||
for pair, info in self._cached_pairs.items():
|
for pair, info in self._cached_pairs.items():
|
||||||
if info.expectancy > float(self.edge_config.get('minimum_expectancy', 0.2)) and \
|
if (info.expectancy > float(self.edge_config.get('minimum_expectancy', 0.2)) and
|
||||||
info.winrate > float(self.edge_config.get('minimum_winrate', 0.60)):
|
info.winrate > float(self.edge_config.get('minimum_winrate', 0.60))):
|
||||||
final.append({
|
final.append({
|
||||||
'Pair': pair,
|
'Pair': pair,
|
||||||
'Winrate': info.winrate,
|
'Winrate': info.winrate,
|
||||||
|
@ -18,6 +18,7 @@ from freqtrade.exchange.exchange import (available_exchanges, ccxt_exchanges,
|
|||||||
from freqtrade.exchange.ftx import Ftx
|
from freqtrade.exchange.ftx import Ftx
|
||||||
from freqtrade.exchange.gateio import Gateio
|
from freqtrade.exchange.gateio import Gateio
|
||||||
from freqtrade.exchange.hitbtc import Hitbtc
|
from freqtrade.exchange.hitbtc import Hitbtc
|
||||||
|
from freqtrade.exchange.huobi import Huobi
|
||||||
from freqtrade.exchange.kraken import Kraken
|
from freqtrade.exchange.kraken import Kraken
|
||||||
from freqtrade.exchange.kucoin import Kucoin
|
from freqtrade.exchange.kucoin import Kucoin
|
||||||
from freqtrade.exchange.okx import Okx
|
from freqtrade.exchange.okx import Okx
|
||||||
|
@ -3,12 +3,8 @@ import logging
|
|||||||
from typing import Dict, List, Tuple
|
from typing import Dict, List, Tuple
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
import ccxt
|
|
||||||
|
|
||||||
from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException,
|
|
||||||
OperationalException, TemporaryError)
|
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
from freqtrade.exchange.common import retrier
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -18,6 +14,7 @@ class Binance(Exchange):
|
|||||||
|
|
||||||
_ft_has: Dict = {
|
_ft_has: Dict = {
|
||||||
"stoploss_on_exchange": True,
|
"stoploss_on_exchange": True,
|
||||||
|
"stoploss_order_types": {"limit": "stop_loss_limit"},
|
||||||
"order_time_in_force": ['gtc', 'fok', 'ioc'],
|
"order_time_in_force": ['gtc', 'fok', 'ioc'],
|
||||||
"time_in_force_parameter": "timeInForce",
|
"time_in_force_parameter": "timeInForce",
|
||||||
"ohlcv_candle_limit": 1000,
|
"ohlcv_candle_limit": 1000,
|
||||||
@ -33,65 +30,6 @@ class Binance(Exchange):
|
|||||||
"""
|
"""
|
||||||
return order['type'] == 'stop_loss_limit' and stop_loss > float(order['info']['stopPrice'])
|
return order['type'] == 'stop_loss_limit' and stop_loss > float(order['info']['stopPrice'])
|
||||||
|
|
||||||
@retrier(retries=0)
|
|
||||||
def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict:
|
|
||||||
"""
|
|
||||||
creates a stoploss limit order.
|
|
||||||
this stoploss-limit is binance-specific.
|
|
||||||
It may work with a limited number of other exchanges, but this has not been tested yet.
|
|
||||||
"""
|
|
||||||
# Limit price threshold: As limit price should always be below stop-price
|
|
||||||
limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99)
|
|
||||||
rate = stop_price * limit_price_pct
|
|
||||||
|
|
||||||
ordertype = "stop_loss_limit"
|
|
||||||
|
|
||||||
stop_price = self.price_to_precision(pair, stop_price)
|
|
||||||
|
|
||||||
# Ensure rate is less than stop price
|
|
||||||
if stop_price <= rate:
|
|
||||||
raise OperationalException(
|
|
||||||
'In stoploss limit order, stop price should be more than limit price')
|
|
||||||
|
|
||||||
if self._config['dry_run']:
|
|
||||||
dry_order = self.create_dry_run_order(
|
|
||||||
pair, ordertype, "sell", amount, stop_price)
|
|
||||||
return dry_order
|
|
||||||
|
|
||||||
try:
|
|
||||||
params = self._params.copy()
|
|
||||||
params.update({'stopPrice': stop_price})
|
|
||||||
|
|
||||||
amount = self.amount_to_precision(pair, amount)
|
|
||||||
|
|
||||||
rate = self.price_to_precision(pair, rate)
|
|
||||||
|
|
||||||
order = self._api.create_order(symbol=pair, type=ordertype, side='sell',
|
|
||||||
amount=amount, price=rate, params=params)
|
|
||||||
logger.info('stoploss limit order added for %s. '
|
|
||||||
'stop price: %s. limit: %s', pair, stop_price, rate)
|
|
||||||
self._log_exchange_response('create_stoploss_order', order)
|
|
||||||
return order
|
|
||||||
except ccxt.InsufficientFunds as e:
|
|
||||||
raise InsufficientFundsError(
|
|
||||||
f'Insufficient funds to create {ordertype} sell order on market {pair}. '
|
|
||||||
f'Tried to sell amount {amount} at rate {rate}. '
|
|
||||||
f'Message: {e}') from e
|
|
||||||
except ccxt.InvalidOrder as e:
|
|
||||||
# Errors:
|
|
||||||
# `binance Order would trigger immediately.`
|
|
||||||
raise InvalidOrderException(
|
|
||||||
f'Could not create {ordertype} sell order on market {pair}. '
|
|
||||||
f'Tried to sell amount {amount} at rate {rate}. '
|
|
||||||
f'Message: {e}') from e
|
|
||||||
except ccxt.DDoSProtection as e:
|
|
||||||
raise DDosProtection(e) from e
|
|
||||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
|
||||||
raise TemporaryError(
|
|
||||||
f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e
|
|
||||||
except ccxt.BaseError as e:
|
|
||||||
raise OperationalException(e) from e
|
|
||||||
|
|
||||||
async def _async_get_historic_ohlcv(self, pair: str, timeframe: str,
|
async def _async_get_historic_ohlcv(self, pair: str, timeframe: str,
|
||||||
since_ms: int, is_new_pair: bool = False,
|
since_ms: int, is_new_pair: bool = False,
|
||||||
raise_: bool = False
|
raise_: bool = False
|
||||||
|
@ -9,7 +9,7 @@ import logging
|
|||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from math import ceil
|
from math import ceil
|
||||||
from typing import Any, Dict, List, Optional, Tuple
|
from typing import Any, Coroutine, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
import ccxt
|
import ccxt
|
||||||
@ -600,7 +600,8 @@ class Exchange:
|
|||||||
# Dry-run methods
|
# Dry-run methods
|
||||||
|
|
||||||
def create_dry_run_order(self, pair: str, ordertype: str, side: str, amount: float,
|
def create_dry_run_order(self, pair: str, ordertype: str, side: str, amount: float,
|
||||||
rate: float, params: Dict = {}) -> Dict[str, Any]:
|
rate: float, params: Dict = {},
|
||||||
|
stop_loss: bool = False) -> Dict[str, Any]:
|
||||||
order_id = f'dry_run_{side}_{datetime.now().timestamp()}'
|
order_id = f'dry_run_{side}_{datetime.now().timestamp()}'
|
||||||
_amount = self.amount_to_precision(pair, amount)
|
_amount = self.amount_to_precision(pair, amount)
|
||||||
dry_order: Dict[str, Any] = {
|
dry_order: Dict[str, Any] = {
|
||||||
@ -616,14 +617,17 @@ class Exchange:
|
|||||||
'remaining': _amount,
|
'remaining': _amount,
|
||||||
'datetime': arrow.utcnow().strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
|
'datetime': arrow.utcnow().strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
|
||||||
'timestamp': arrow.utcnow().int_timestamp * 1000,
|
'timestamp': arrow.utcnow().int_timestamp * 1000,
|
||||||
'status': "closed" if ordertype == "market" else "open",
|
'status': "closed" if ordertype == "market" and not stop_loss else "open",
|
||||||
'fee': None,
|
'fee': None,
|
||||||
'info': {}
|
'info': {}
|
||||||
}
|
}
|
||||||
if dry_order["type"] in ["stop_loss_limit", "stop-loss-limit"]:
|
if stop_loss:
|
||||||
dry_order["info"] = {"stopPrice": dry_order["price"]}
|
dry_order["info"] = {"stopPrice": dry_order["price"]}
|
||||||
|
dry_order["stopPrice"] = dry_order["price"]
|
||||||
|
# Workaround to avoid filling stoploss orders immediately
|
||||||
|
dry_order["ft_order_type"] = "stoploss"
|
||||||
|
|
||||||
if dry_order["type"] == "market":
|
if dry_order["type"] == "market" and not dry_order.get("ft_order_type"):
|
||||||
# Update market order pricing
|
# Update market order pricing
|
||||||
average = self.get_dry_market_fill_price(pair, side, amount, rate)
|
average = self.get_dry_market_fill_price(pair, side, amount, rate)
|
||||||
dry_order.update({
|
dry_order.update({
|
||||||
@ -714,7 +718,9 @@ class Exchange:
|
|||||||
"""
|
"""
|
||||||
Check dry-run limit order fill and update fee (if it filled).
|
Check dry-run limit order fill and update fee (if it filled).
|
||||||
"""
|
"""
|
||||||
if order['status'] != "closed" and order['type'] in ["limit"]:
|
if (order['status'] != "closed"
|
||||||
|
and order['type'] in ["limit"]
|
||||||
|
and not order.get('ft_order_type')):
|
||||||
pair = order['symbol']
|
pair = order['symbol']
|
||||||
if self._is_dry_limit_order_filled(pair, order['side'], order['price']):
|
if self._is_dry_limit_order_filled(pair, order['side'], order['price']):
|
||||||
order.update({
|
order.update({
|
||||||
@ -791,25 +797,96 @@ class Exchange:
|
|||||||
"""
|
"""
|
||||||
raise OperationalException(f"stoploss is not implemented for {self.name}.")
|
raise OperationalException(f"stoploss is not implemented for {self.name}.")
|
||||||
|
|
||||||
|
def _get_stop_params(self, ordertype: str, stop_price: float) -> Dict:
|
||||||
|
params = self._params.copy()
|
||||||
|
# Verify if stopPrice works for your exchange!
|
||||||
|
params.update({'stopPrice': stop_price})
|
||||||
|
return params
|
||||||
|
|
||||||
|
@retrier(retries=0)
|
||||||
def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict:
|
def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict:
|
||||||
"""
|
"""
|
||||||
creates a stoploss order.
|
creates a stoploss order.
|
||||||
|
requires `_ft_has['stoploss_order_types']` to be set as a dict mapping limit and market
|
||||||
|
to the corresponding exchange type.
|
||||||
|
|
||||||
The precise ordertype is determined by the order_types dict or exchange default.
|
The precise ordertype is determined by the order_types dict or exchange default.
|
||||||
Since ccxt does not unify stoploss-limit orders yet, this needs to be implemented in each
|
|
||||||
exchange's subclass.
|
|
||||||
The exception below should never raise, since we disallow
|
The exception below should never raise, since we disallow
|
||||||
starting the bot in validate_ordertypes()
|
starting the bot in validate_ordertypes()
|
||||||
Note: Changes to this interface need to be applied to all sub-classes too.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
This may work with a limited number of other exchanges, but correct working
|
||||||
|
needs to be tested individually.
|
||||||
|
WARNING: setting `stoploss_on_exchange` to True will NOT auto-enable stoploss on exchange.
|
||||||
|
`stoploss_adjust` must still be implemented for this to work.
|
||||||
|
"""
|
||||||
|
if not self._ft_has['stoploss_on_exchange']:
|
||||||
raise OperationalException(f"stoploss is not implemented for {self.name}.")
|
raise OperationalException(f"stoploss is not implemented for {self.name}.")
|
||||||
|
|
||||||
|
user_order_type = order_types.get('stoploss', 'market')
|
||||||
|
if user_order_type in self._ft_has["stoploss_order_types"].keys():
|
||||||
|
ordertype = self._ft_has["stoploss_order_types"][user_order_type]
|
||||||
|
else:
|
||||||
|
# Otherwise pick only one available
|
||||||
|
ordertype = list(self._ft_has["stoploss_order_types"].values())[0]
|
||||||
|
user_order_type = list(self._ft_has["stoploss_order_types"].keys())[0]
|
||||||
|
|
||||||
|
stop_price_norm = self.price_to_precision(pair, stop_price)
|
||||||
|
rate = None
|
||||||
|
if user_order_type == 'limit':
|
||||||
|
# Limit price threshold: As limit price should always be below stop-price
|
||||||
|
limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99)
|
||||||
|
rate = stop_price * limit_price_pct
|
||||||
|
|
||||||
|
# Ensure rate is less than stop price
|
||||||
|
if stop_price_norm <= rate:
|
||||||
|
raise OperationalException(
|
||||||
|
'In stoploss limit order, stop price should be more than limit price')
|
||||||
|
rate = self.price_to_precision(pair, rate)
|
||||||
|
|
||||||
|
if self._config['dry_run']:
|
||||||
|
dry_order = self.create_dry_run_order(
|
||||||
|
pair, ordertype, "sell", amount, stop_price_norm, stop_loss=True)
|
||||||
|
return dry_order
|
||||||
|
|
||||||
|
try:
|
||||||
|
params = self._get_stop_params(ordertype=ordertype, stop_price=stop_price_norm)
|
||||||
|
|
||||||
|
amount = self.amount_to_precision(pair, amount)
|
||||||
|
|
||||||
|
order = self._api.create_order(symbol=pair, type=ordertype, side='sell',
|
||||||
|
amount=amount, price=rate, params=params)
|
||||||
|
logger.info(f"stoploss {user_order_type} order added for {pair}. "
|
||||||
|
f"stop price: {stop_price}. limit: {rate}")
|
||||||
|
self._log_exchange_response('create_stoploss_order', order)
|
||||||
|
return order
|
||||||
|
except ccxt.InsufficientFunds as e:
|
||||||
|
raise InsufficientFundsError(
|
||||||
|
f'Insufficient funds to create {ordertype} sell order on market {pair}. '
|
||||||
|
f'Tried to sell amount {amount} at rate {rate}. '
|
||||||
|
f'Message: {e}') from e
|
||||||
|
except ccxt.InvalidOrder as e:
|
||||||
|
# Errors:
|
||||||
|
# `Order would trigger immediately.`
|
||||||
|
raise InvalidOrderException(
|
||||||
|
f'Could not create {ordertype} sell order on market {pair}. '
|
||||||
|
f'Tried to sell amount {amount} at rate {rate}. '
|
||||||
|
f'Message: {e}') from e
|
||||||
|
except ccxt.DDoSProtection as e:
|
||||||
|
raise DDosProtection(e) from e
|
||||||
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
|
raise TemporaryError(
|
||||||
|
f"Could not place stoploss order due to {e.__class__.__name__}. "
|
||||||
|
f"Message: {e}") from e
|
||||||
|
except ccxt.BaseError as e:
|
||||||
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
@retrier(retries=API_FETCH_ORDER_RETRY_COUNT)
|
@retrier(retries=API_FETCH_ORDER_RETRY_COUNT)
|
||||||
def fetch_order(self, order_id: str, pair: str) -> Dict:
|
def fetch_order(self, order_id: str, pair: str, params={}) -> Dict:
|
||||||
if self._config['dry_run']:
|
if self._config['dry_run']:
|
||||||
return self.fetch_dry_run_order(order_id)
|
return self.fetch_dry_run_order(order_id)
|
||||||
try:
|
try:
|
||||||
order = self._api.fetch_order(order_id, pair)
|
order = self._api.fetch_order(order_id, pair, params=params)
|
||||||
self._log_exchange_response('fetch_order', order)
|
self._log_exchange_response('fetch_order', order)
|
||||||
return order
|
return order
|
||||||
except ccxt.OrderNotFound as e:
|
except ccxt.OrderNotFound as e:
|
||||||
@ -852,7 +929,7 @@ class Exchange:
|
|||||||
and order.get('filled') == 0.0)
|
and order.get('filled') == 0.0)
|
||||||
|
|
||||||
@retrier
|
@retrier
|
||||||
def cancel_order(self, order_id: str, pair: str) -> Dict:
|
def cancel_order(self, order_id: str, pair: str, params={}) -> Dict:
|
||||||
if self._config['dry_run']:
|
if self._config['dry_run']:
|
||||||
try:
|
try:
|
||||||
order = self.fetch_dry_run_order(order_id)
|
order = self.fetch_dry_run_order(order_id)
|
||||||
@ -863,7 +940,7 @@ class Exchange:
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
order = self._api.cancel_order(order_id, pair)
|
order = self._api.cancel_order(order_id, pair, params=params)
|
||||||
self._log_exchange_response('cancel_order', order)
|
self._log_exchange_response('cancel_order', order)
|
||||||
return order
|
return order
|
||||||
except ccxt.InvalidOrder as e:
|
except ccxt.InvalidOrder as e:
|
||||||
@ -1294,6 +1371,22 @@ class Exchange:
|
|||||||
data = sorted(data, key=lambda x: x[0])
|
data = sorted(data, key=lambda x: x[0])
|
||||||
return pair, timeframe, data
|
return pair, timeframe, data
|
||||||
|
|
||||||
|
def _build_coroutine(self, pair: str, timeframe: str, since_ms: Optional[int]) -> Coroutine:
|
||||||
|
if not since_ms and self.required_candle_call_count > 1:
|
||||||
|
# Multiple calls for one pair - to get more history
|
||||||
|
one_call = timeframe_to_msecs(timeframe) * self.ohlcv_candle_limit(timeframe)
|
||||||
|
move_to = one_call * self.required_candle_call_count
|
||||||
|
now = timeframe_to_next_date(timeframe)
|
||||||
|
since_ms = int((now - timedelta(seconds=move_to // 1000)).timestamp() * 1000)
|
||||||
|
|
||||||
|
if since_ms:
|
||||||
|
return self._async_get_historic_ohlcv(
|
||||||
|
pair, timeframe, since_ms=since_ms, raise_=True)
|
||||||
|
else:
|
||||||
|
# One call ... "regular" refresh
|
||||||
|
return self._async_get_candle_history(
|
||||||
|
pair, timeframe, since_ms=since_ms)
|
||||||
|
|
||||||
def refresh_latest_ohlcv(self, pair_list: ListPairsWithTimeframes, *,
|
def refresh_latest_ohlcv(self, pair_list: ListPairsWithTimeframes, *,
|
||||||
since_ms: Optional[int] = None, cache: bool = True
|
since_ms: Optional[int] = None, cache: bool = True
|
||||||
) -> Dict[Tuple[str, str], DataFrame]:
|
) -> Dict[Tuple[str, str], DataFrame]:
|
||||||
@ -1312,22 +1405,15 @@ class Exchange:
|
|||||||
cached_pairs = []
|
cached_pairs = []
|
||||||
# Gather coroutines to run
|
# Gather coroutines to run
|
||||||
for pair, timeframe in set(pair_list):
|
for pair, timeframe in set(pair_list):
|
||||||
|
if timeframe not in self.timeframes:
|
||||||
|
logger.warning(
|
||||||
|
f"Cannot download ({pair}, {timeframe}) combination as this timeframe is "
|
||||||
|
f"not available on {self.name}. Available timeframes are "
|
||||||
|
f"{', '.join(self.timeframes)}.")
|
||||||
|
continue
|
||||||
if ((pair, timeframe) not in self._klines or not cache
|
if ((pair, timeframe) not in self._klines or not cache
|
||||||
or self._now_is_time_to_refresh(pair, timeframe)):
|
or self._now_is_time_to_refresh(pair, timeframe)):
|
||||||
if not since_ms and self.required_candle_call_count > 1:
|
input_coroutines.append(self._build_coroutine(pair, timeframe, since_ms))
|
||||||
# Multiple calls for one pair - to get more history
|
|
||||||
one_call = timeframe_to_msecs(timeframe) * self.ohlcv_candle_limit(timeframe)
|
|
||||||
move_to = one_call * self.required_candle_call_count
|
|
||||||
now = timeframe_to_next_date(timeframe)
|
|
||||||
since_ms = int((now - timedelta(seconds=move_to // 1000)).timestamp() * 1000)
|
|
||||||
|
|
||||||
if since_ms:
|
|
||||||
input_coroutines.append(self._async_get_historic_ohlcv(
|
|
||||||
pair, timeframe, since_ms=since_ms, raise_=True))
|
|
||||||
else:
|
|
||||||
# One call ... "regular" refresh
|
|
||||||
input_coroutines.append(self._async_get_candle_history(
|
|
||||||
pair, timeframe, since_ms=since_ms))
|
|
||||||
else:
|
else:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"Using cached candle (OHLCV) data for pair %s, timeframe %s ...",
|
"Using cached candle (OHLCV) data for pair %s, timeframe %s ...",
|
||||||
@ -1587,7 +1673,7 @@ def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = Non
|
|||||||
|
|
||||||
|
|
||||||
def is_exchange_officially_supported(exchange_name: str) -> bool:
|
def is_exchange_officially_supported(exchange_name: str) -> bool:
|
||||||
return exchange_name in ['bittrex', 'binance', 'kraken', 'ftx', 'gateio', 'okx']
|
return exchange_name in ['binance', 'bittrex', 'ftx', 'gateio', 'huobi', 'kraken', 'okx']
|
||||||
|
|
||||||
|
|
||||||
def ccxt_exchanges(ccxt_module: CcxtModuleType = None) -> List[str]:
|
def ccxt_exchanges(ccxt_module: CcxtModuleType = None) -> List[str]:
|
||||||
|
@ -56,7 +56,7 @@ class Ftx(Exchange):
|
|||||||
|
|
||||||
if self._config['dry_run']:
|
if self._config['dry_run']:
|
||||||
dry_order = self.create_dry_run_order(
|
dry_order = self.create_dry_run_order(
|
||||||
pair, ordertype, "sell", amount, stop_price)
|
pair, ordertype, "sell", amount, stop_price, stop_loss=True)
|
||||||
return dry_order
|
return dry_order
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -22,13 +22,34 @@ class Gateio(Exchange):
|
|||||||
_ft_has: Dict = {
|
_ft_has: Dict = {
|
||||||
"ohlcv_candle_limit": 1000,
|
"ohlcv_candle_limit": 1000,
|
||||||
"ohlcv_volume_currency": "quote",
|
"ohlcv_volume_currency": "quote",
|
||||||
|
"stoploss_order_types": {"limit": "limit"},
|
||||||
|
"stoploss_on_exchange": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
_headers = {'X-Gate-Channel-Id': 'freqtrade'}
|
|
||||||
|
|
||||||
def validate_ordertypes(self, order_types: Dict) -> None:
|
def validate_ordertypes(self, order_types: Dict) -> None:
|
||||||
super().validate_ordertypes(order_types)
|
super().validate_ordertypes(order_types)
|
||||||
|
|
||||||
if any(v == 'market' for k, v in order_types.items()):
|
if any(v == 'market' for k, v in order_types.items()):
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
f'Exchange {self.name} does not support market orders.')
|
f'Exchange {self.name} does not support market orders.')
|
||||||
|
|
||||||
|
def fetch_stoploss_order(self, order_id: str, pair: str, params={}) -> 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:
|
||||||
|
return self.cancel_order(
|
||||||
|
order_id=order_id,
|
||||||
|
pair=pair,
|
||||||
|
params={'stop': True}
|
||||||
|
)
|
||||||
|
|
||||||
|
def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool:
|
||||||
|
"""
|
||||||
|
Verify stop_loss against stoploss-order value (limit or price)
|
||||||
|
Returns True if adjustment is necessary.
|
||||||
|
"""
|
||||||
|
return stop_loss > float(order['stopPrice'])
|
||||||
|
39
freqtrade/exchange/huobi.py
Normal file
39
freqtrade/exchange/huobi.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
""" Huobi exchange subclass """
|
||||||
|
import logging
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
from freqtrade.exchange import Exchange
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Huobi(Exchange):
|
||||||
|
"""
|
||||||
|
Huobi exchange class. Contains adjustments needed for Freqtrade to work
|
||||||
|
with this exchange.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_ft_has: Dict = {
|
||||||
|
"stoploss_on_exchange": True,
|
||||||
|
"stoploss_order_types": {"limit": "stop-limit"},
|
||||||
|
"ohlcv_candle_limit": 1000,
|
||||||
|
"l2_limit_range": [5, 10, 20],
|
||||||
|
"l2_limit_range_required": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool:
|
||||||
|
"""
|
||||||
|
Verify stop_loss against stoploss-order value (limit or price)
|
||||||
|
Returns True if adjustment is necessary.
|
||||||
|
"""
|
||||||
|
return order['type'] == 'stop' and stop_loss > float(order['stopPrice'])
|
||||||
|
|
||||||
|
def _get_stop_params(self, ordertype: str, stop_price: float) -> Dict:
|
||||||
|
|
||||||
|
params = self._params.copy()
|
||||||
|
params.update({
|
||||||
|
"stopPrice": stop_price,
|
||||||
|
"operator": "lte",
|
||||||
|
})
|
||||||
|
return params
|
@ -86,6 +86,8 @@ class Kraken(Exchange):
|
|||||||
"""
|
"""
|
||||||
Creates a stoploss market order.
|
Creates a stoploss market order.
|
||||||
Stoploss market orders is the only stoploss type supported by kraken.
|
Stoploss market orders is the only stoploss type supported by kraken.
|
||||||
|
TODO: investigate if this can be combined with generic implementation
|
||||||
|
(careful, prices are reversed)
|
||||||
"""
|
"""
|
||||||
params = self._params.copy()
|
params = self._params.copy()
|
||||||
|
|
||||||
@ -101,7 +103,7 @@ class Kraken(Exchange):
|
|||||||
|
|
||||||
if self._config['dry_run']:
|
if self._config['dry_run']:
|
||||||
dry_order = self.create_dry_run_order(
|
dry_order = self.create_dry_run_order(
|
||||||
pair, ordertype, "sell", amount, stop_price)
|
pair, ordertype, "sell", amount, stop_price, stop_loss=True)
|
||||||
return dry_order
|
return dry_order
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -19,8 +19,26 @@ class Kucoin(Exchange):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
_ft_has: Dict = {
|
_ft_has: Dict = {
|
||||||
|
"stoploss_on_exchange": True,
|
||||||
|
"stoploss_order_types": {"limit": "limit", "market": "market"},
|
||||||
"l2_limit_range": [20, 100],
|
"l2_limit_range": [20, 100],
|
||||||
"l2_limit_range_required": False,
|
"l2_limit_range_required": False,
|
||||||
"order_time_in_force": ['gtc', 'fok', 'ioc'],
|
"order_time_in_force": ['gtc', 'fok', 'ioc'],
|
||||||
"time_in_force_parameter": "timeInForce",
|
"time_in_force_parameter": "timeInForce",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool:
|
||||||
|
"""
|
||||||
|
Verify stop_loss against stoploss-order value (limit or price)
|
||||||
|
Returns True if adjustment is necessary.
|
||||||
|
"""
|
||||||
|
return order['info'].get('stop') is not None and stop_loss > float(order['stopPrice'])
|
||||||
|
|
||||||
|
def _get_stop_params(self, ordertype: str, stop_price: float) -> Dict:
|
||||||
|
|
||||||
|
params = self._params.copy()
|
||||||
|
params.update({
|
||||||
|
'stopPrice': stop_price,
|
||||||
|
'stop': 'loss'
|
||||||
|
})
|
||||||
|
return params
|
||||||
|
@ -542,7 +542,6 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
entry_tag=buy_tag):
|
entry_tag=buy_tag):
|
||||||
logger.info(f"User requested abortion of buying {pair}")
|
logger.info(f"User requested abortion of buying {pair}")
|
||||||
return False
|
return False
|
||||||
amount = self.exchange.amount_to_precision(pair, amount)
|
|
||||||
order = self.exchange.create_order(pair=pair, ordertype=order_type, side="buy",
|
order = self.exchange.create_order(pair=pair, ordertype=order_type, side="buy",
|
||||||
amount=amount, rate=enter_limit_requested,
|
amount=amount, rate=enter_limit_requested,
|
||||||
time_in_force=time_in_force)
|
time_in_force=time_in_force)
|
||||||
@ -874,11 +873,15 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
stop_price = trade.open_rate * (1 + stoploss)
|
stop_price = trade.open_rate * (1 + stoploss)
|
||||||
|
|
||||||
if self.create_stoploss_order(trade=trade, stop_price=stop_price):
|
if self.create_stoploss_order(trade=trade, stop_price=stop_price):
|
||||||
|
# The above will return False if the placement failed and the trade was force-sold.
|
||||||
|
# in which case the trade will be closed - which we must check below.
|
||||||
trade.stoploss_last_update = datetime.utcnow()
|
trade.stoploss_last_update = datetime.utcnow()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# If stoploss order is canceled for some reason we add it
|
# If stoploss order is canceled for some reason we add it
|
||||||
if stoploss_order and stoploss_order['status'] in ('canceled', 'cancelled'):
|
if (trade.is_open
|
||||||
|
and stoploss_order
|
||||||
|
and stoploss_order['status'] in ('canceled', 'cancelled')):
|
||||||
if self.create_stoploss_order(trade=trade, stop_price=trade.stop_loss):
|
if self.create_stoploss_order(trade=trade, stop_price=trade.stop_loss):
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
@ -888,7 +891,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
# Finally we check if stoploss on exchange should be moved up because of trailing.
|
# Finally we check if stoploss on exchange should be moved up because of trailing.
|
||||||
# Triggered Orders are now real orders - so don't replace stoploss anymore
|
# Triggered Orders are now real orders - so don't replace stoploss anymore
|
||||||
if (
|
if (
|
||||||
stoploss_order
|
trade.is_open and stoploss_order
|
||||||
and stoploss_order.get('status_stop') != 'triggered'
|
and stoploss_order.get('status_stop') != 'triggered'
|
||||||
and (self.config.get('trailing_stop', False)
|
and (self.config.get('trailing_stop', False)
|
||||||
or self.config.get('use_custom_stoploss', False))
|
or self.config.get('use_custom_stoploss', False))
|
||||||
@ -900,7 +903,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def handle_trailing_stoploss_on_exchange(self, trade: Trade, order: dict) -> None:
|
def handle_trailing_stoploss_on_exchange(self, trade: Trade, order: Dict) -> None:
|
||||||
"""
|
"""
|
||||||
Check to see if stoploss on exchange should be updated
|
Check to see if stoploss on exchange should be updated
|
||||||
in case of trailing stoploss on exchange
|
in case of trailing stoploss on exchange
|
||||||
@ -908,7 +911,9 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
:param order: Current on exchange stoploss order
|
:param order: Current on exchange stoploss order
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
if self.exchange.stoploss_adjust(trade.stop_loss, order):
|
stoploss_norm = self.exchange.price_to_precision(trade.pair, trade.stop_loss)
|
||||||
|
|
||||||
|
if self.exchange.stoploss_adjust(stoploss_norm, order):
|
||||||
# we check if the update is necessary
|
# we check if the update is necessary
|
||||||
update_beat = self.strategy.order_types.get('stoploss_on_exchange_interval', 60)
|
update_beat = self.strategy.order_types.get('stoploss_on_exchange_interval', 60)
|
||||||
if (datetime.utcnow() - trade.stoploss_last_update).total_seconds() >= update_beat:
|
if (datetime.utcnow() - trade.stoploss_last_update).total_seconds() >= update_beat:
|
||||||
@ -1109,6 +1114,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
trade.close_date = None
|
trade.close_date = None
|
||||||
trade.is_open = True
|
trade.is_open = True
|
||||||
trade.open_order_id = None
|
trade.open_order_id = None
|
||||||
|
trade.sell_reason = None
|
||||||
cancelled = True
|
cancelled = True
|
||||||
else:
|
else:
|
||||||
# TODO: figure out how to handle partially complete sell orders
|
# TODO: figure out how to handle partially complete sell orders
|
||||||
@ -1170,8 +1176,8 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
|
|
||||||
# if stoploss is on exchange and we are on dry_run mode,
|
# if stoploss is on exchange and we are on dry_run mode,
|
||||||
# we consider the sell price stop price
|
# we consider the sell price stop price
|
||||||
if self.config['dry_run'] and sell_type == 'stoploss' \
|
if (self.config['dry_run'] and sell_type == 'stoploss'
|
||||||
and self.strategy.order_types['stoploss_on_exchange']:
|
and self.strategy.order_types['stoploss_on_exchange']):
|
||||||
limit = trade.stop_loss
|
limit = trade.stop_loss
|
||||||
|
|
||||||
# set custom_exit_price if available
|
# set custom_exit_price if available
|
||||||
@ -1422,14 +1428,14 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
def handle_order_fee(self, trade: Trade, order_obj: Order, order: Dict[str, Any]) -> None:
|
def handle_order_fee(self, trade: Trade, order_obj: Order, order: Dict[str, Any]) -> None:
|
||||||
# Try update amount (binance-fix)
|
# Try update amount (binance-fix)
|
||||||
try:
|
try:
|
||||||
new_amount = self.get_real_amount(trade, order)
|
new_amount = self.get_real_amount(trade, order, order_obj)
|
||||||
if not isclose(safe_value_fallback(order, 'filled', 'amount'), new_amount,
|
if not isclose(safe_value_fallback(order, 'filled', 'amount'), new_amount,
|
||||||
abs_tol=constants.MATH_CLOSE_PREC):
|
abs_tol=constants.MATH_CLOSE_PREC):
|
||||||
order_obj.ft_fee_base = trade.amount - new_amount
|
order_obj.ft_fee_base = trade.amount - new_amount
|
||||||
except DependencyException as exception:
|
except DependencyException as exception:
|
||||||
logger.warning("Could not update trade amount: %s", exception)
|
logger.warning("Could not update trade amount: %s", exception)
|
||||||
|
|
||||||
def get_real_amount(self, trade: Trade, order: Dict) -> float:
|
def get_real_amount(self, trade: Trade, order: Dict, order_obj: Order) -> float:
|
||||||
"""
|
"""
|
||||||
Detect and update trade fee.
|
Detect and update trade fee.
|
||||||
Calls trade.update_fee() upon correct detection.
|
Calls trade.update_fee() upon correct detection.
|
||||||
@ -1447,7 +1453,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
# use fee from order-dict if possible
|
# use fee from order-dict if possible
|
||||||
if self.exchange.order_has_fee(order):
|
if self.exchange.order_has_fee(order):
|
||||||
fee_cost, fee_currency, fee_rate = self.exchange.extract_cost_curr_rate(order)
|
fee_cost, fee_currency, fee_rate = self.exchange.extract_cost_curr_rate(order)
|
||||||
logger.info(f"Fee for Trade {trade} [{order.get('side')}]: "
|
logger.info(f"Fee for Trade {trade} [{order_obj.ft_order_side}]: "
|
||||||
f"{fee_cost:.8g} {fee_currency} - rate: {fee_rate}")
|
f"{fee_cost:.8g} {fee_currency} - rate: {fee_rate}")
|
||||||
if fee_rate is None or fee_rate < 0.02:
|
if fee_rate is None or fee_rate < 0.02:
|
||||||
# Reject all fees that report as > 2%.
|
# Reject all fees that report as > 2%.
|
||||||
@ -1459,17 +1465,18 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
return self.apply_fee_conditional(trade, trade_base_currency,
|
return self.apply_fee_conditional(trade, trade_base_currency,
|
||||||
amount=order_amount, fee_abs=fee_cost)
|
amount=order_amount, fee_abs=fee_cost)
|
||||||
return order_amount
|
return order_amount
|
||||||
return self.fee_detection_from_trades(trade, order, order_amount, order.get('trades', []))
|
return self.fee_detection_from_trades(
|
||||||
|
trade, order, order_obj, order_amount, order.get('trades', []))
|
||||||
|
|
||||||
def fee_detection_from_trades(self, trade: Trade, order: Dict, order_amount: float,
|
def fee_detection_from_trades(self, trade: Trade, order: Dict, order_obj: Order,
|
||||||
trades: List) -> float:
|
order_amount: float, trades: List) -> float:
|
||||||
"""
|
"""
|
||||||
fee-detection fallback to Trades.
|
fee-detection fallback to Trades.
|
||||||
Either uses provided trades list or the result of fetch_my_trades to get correct fee.
|
Either uses provided trades list or the result of fetch_my_trades to get correct fee.
|
||||||
"""
|
"""
|
||||||
if not trades:
|
if not trades:
|
||||||
trades = self.exchange.get_trades_for_order(
|
trades = self.exchange.get_trades_for_order(
|
||||||
self.exchange.get_order_id_conditional(order), trade.pair, trade.open_date)
|
self.exchange.get_order_id_conditional(order), trade.pair, order_obj.order_date)
|
||||||
|
|
||||||
if len(trades) == 0:
|
if len(trades) == 0:
|
||||||
logger.info("Applying fee on amount for %s failed: myTrade-Dict empty found", trade)
|
logger.info("Applying fee on amount for %s failed: myTrade-Dict empty found", trade)
|
||||||
|
@ -87,7 +87,7 @@ class Backtesting:
|
|||||||
validate_config_consistency(self.config)
|
validate_config_consistency(self.config)
|
||||||
|
|
||||||
if "timeframe" not in self.config:
|
if "timeframe" not in self.config:
|
||||||
raise OperationalException("Timeframe (ticker interval) needs to be set in either "
|
raise OperationalException("Timeframe needs to be set in either "
|
||||||
"configuration or as cli argument `--timeframe 5m`")
|
"configuration or as cli argument `--timeframe 5m`")
|
||||||
self.timeframe = str(self.config.get('timeframe'))
|
self.timeframe = str(self.config.get('timeframe'))
|
||||||
self.timeframe_min = timeframe_to_minutes(self.timeframe)
|
self.timeframe_min = timeframe_to_minutes(self.timeframe)
|
||||||
|
@ -29,15 +29,13 @@ class IHyperOpt(ABC):
|
|||||||
Class attributes you can use:
|
Class attributes you can use:
|
||||||
timeframe -> int: value of the timeframe to use for the strategy
|
timeframe -> int: value of the timeframe to use for the strategy
|
||||||
"""
|
"""
|
||||||
ticker_interval: str # DEPRECATED
|
|
||||||
timeframe: str
|
timeframe: str
|
||||||
strategy: IStrategy
|
strategy: IStrategy
|
||||||
|
|
||||||
def __init__(self, config: dict) -> None:
|
def __init__(self, config: dict) -> None:
|
||||||
self.config = config
|
self.config = config
|
||||||
|
|
||||||
# Assign ticker_interval to be used in hyperopt
|
# Assign timeframe to be used in hyperopt
|
||||||
IHyperOpt.ticker_interval = str(config['timeframe']) # DEPRECATED
|
|
||||||
IHyperOpt.timeframe = str(config['timeframe'])
|
IHyperOpt.timeframe = str(config['timeframe'])
|
||||||
|
|
||||||
def generate_estimator(self, dimensions: List[Dimension], **kwargs) -> EstimatorType:
|
def generate_estimator(self, dimensions: List[Dimension], **kwargs) -> EstimatorType:
|
||||||
@ -192,7 +190,7 @@ class IHyperOpt(ABC):
|
|||||||
Categorical([True, False], name='trailing_only_offset_is_reached'),
|
Categorical([True, False], name='trailing_only_offset_is_reached'),
|
||||||
]
|
]
|
||||||
|
|
||||||
# This is needed for proper unpickling the class attribute ticker_interval
|
# This is needed for proper unpickling the class attribute timeframe
|
||||||
# which is set to the actual value by the resolver.
|
# which is set to the actual value by the resolver.
|
||||||
# Why do I still need such shamanic mantras in modern python?
|
# Why do I still need such shamanic mantras in modern python?
|
||||||
def __getstate__(self):
|
def __getstate__(self):
|
||||||
@ -202,5 +200,4 @@ class IHyperOpt(ABC):
|
|||||||
|
|
||||||
def __setstate__(self, state):
|
def __setstate__(self, state):
|
||||||
self.__dict__.update(state)
|
self.__dict__.update(state)
|
||||||
IHyperOpt.ticker_interval = state['timeframe']
|
|
||||||
IHyperOpt.timeframe = state['timeframe']
|
IHyperOpt.timeframe = state['timeframe']
|
||||||
|
@ -174,16 +174,17 @@ def drop_orders_table(engine, table_back_name: str):
|
|||||||
def migrate_orders_table(engine, table_back_name: str, cols_order: List):
|
def migrate_orders_table(engine, table_back_name: str, cols_order: List):
|
||||||
|
|
||||||
ft_fee_base = get_column_def(cols_order, 'ft_fee_base', 'null')
|
ft_fee_base = get_column_def(cols_order, 'ft_fee_base', 'null')
|
||||||
|
average = get_column_def(cols_order, 'average', 'null')
|
||||||
|
|
||||||
# let SQLAlchemy create the schema as required
|
# let SQLAlchemy create the schema as required
|
||||||
with engine.begin() as connection:
|
with engine.begin() as connection:
|
||||||
connection.execute(text(f"""
|
connection.execute(text(f"""
|
||||||
insert into orders ( id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id,
|
insert into orders ( id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id,
|
||||||
status, symbol, order_type, side, price, amount, filled, average, remaining, cost,
|
status, symbol, order_type, side, price, amount, filled, average, remaining,
|
||||||
order_date, order_filled_date, order_update_date, ft_fee_base)
|
cost, order_date, order_filled_date, order_update_date, ft_fee_base)
|
||||||
select id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id,
|
select id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id,
|
||||||
status, symbol, order_type, side, price, amount, filled, null average, remaining, cost,
|
status, symbol, order_type, side, price, amount, filled, {average} average, remaining,
|
||||||
order_date, order_filled_date, order_update_date, {ft_fee_base}
|
cost, order_date, order_filled_date, order_update_date, {ft_fee_base} ft_fee_base
|
||||||
from {table_back_name}
|
from {table_back_name}
|
||||||
"""))
|
"""))
|
||||||
|
|
||||||
|
@ -120,7 +120,7 @@ class Order(_DECL_BASE):
|
|||||||
ft_pair: str = Column(String(25), nullable=False)
|
ft_pair: str = Column(String(25), nullable=False)
|
||||||
ft_is_open = Column(Boolean, nullable=False, default=True, index=True)
|
ft_is_open = Column(Boolean, nullable=False, default=True, index=True)
|
||||||
|
|
||||||
order_id = Column(String(255), nullable=False, index=True)
|
order_id: str = Column(String(255), nullable=False, index=True)
|
||||||
status = Column(String(255), nullable=True)
|
status = Column(String(255), nullable=True)
|
||||||
symbol = Column(String(25), nullable=True)
|
symbol = Column(String(25), nullable=True)
|
||||||
order_type: str = Column(String(50), nullable=True)
|
order_type: str = Column(String(50), nullable=True)
|
||||||
@ -193,6 +193,9 @@ class Order(_DECL_BASE):
|
|||||||
|
|
||||||
def to_json(self) -> Dict[str, Any]:
|
def to_json(self) -> Dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
|
'pair': self.ft_pair,
|
||||||
|
'order_id': self.order_id,
|
||||||
|
'status': self.status,
|
||||||
'amount': self.amount,
|
'amount': self.amount,
|
||||||
'average': round(self.average, 8) if self.average else 0,
|
'average': round(self.average, 8) if self.average else 0,
|
||||||
'safe_price': self.safe_price,
|
'safe_price': self.safe_price,
|
||||||
@ -209,10 +212,8 @@ class Order(_DECL_BASE):
|
|||||||
'order_filled_timestamp': int(self.order_filled_date.replace(
|
'order_filled_timestamp': int(self.order_filled_date.replace(
|
||||||
tzinfo=timezone.utc).timestamp() * 1000) if self.order_filled_date else None,
|
tzinfo=timezone.utc).timestamp() * 1000) if self.order_filled_date else None,
|
||||||
'order_type': self.order_type,
|
'order_type': self.order_type,
|
||||||
'pair': self.ft_pair,
|
|
||||||
'price': self.price,
|
'price': self.price,
|
||||||
'remaining': self.remaining,
|
'remaining': self.remaining,
|
||||||
'status': self.status,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def close_bt_order(self, close_date: datetime):
|
def close_bt_order(self, close_date: datetime):
|
||||||
@ -303,7 +304,7 @@ class LocalTrade():
|
|||||||
# absolute value of the initial stop loss
|
# absolute value of the initial stop loss
|
||||||
initial_stop_loss: float = 0.0
|
initial_stop_loss: float = 0.0
|
||||||
# percentage value of the initial stop loss
|
# percentage value of the initial stop loss
|
||||||
initial_stop_loss_pct: float = 0.0
|
initial_stop_loss_pct: Optional[float] = None
|
||||||
# stoploss order id which is on exchange
|
# stoploss order id which is on exchange
|
||||||
stoploss_order_id: Optional[str] = None
|
stoploss_order_id: Optional[str] = None
|
||||||
# last update time of the stoploss order on exchange
|
# last update time of the stoploss order on exchange
|
||||||
@ -339,14 +340,7 @@ class LocalTrade():
|
|||||||
|
|
||||||
def to_json(self) -> Dict[str, Any]:
|
def to_json(self) -> Dict[str, Any]:
|
||||||
filled_orders = self.select_filled_orders()
|
filled_orders = self.select_filled_orders()
|
||||||
filled_entries = []
|
orders = [order.to_json() for order in filled_orders]
|
||||||
filled_exits = []
|
|
||||||
if len(filled_orders) > 0:
|
|
||||||
for order in filled_orders:
|
|
||||||
if order.ft_order_side == 'buy':
|
|
||||||
filled_entries.append(order.to_json())
|
|
||||||
if order.ft_order_side == 'sell':
|
|
||||||
filled_exits.append(order.to_json())
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'trade_id': self.id,
|
'trade_id': self.id,
|
||||||
@ -411,8 +405,7 @@ class LocalTrade():
|
|||||||
'max_rate': self.max_rate,
|
'max_rate': self.max_rate,
|
||||||
|
|
||||||
'open_order_id': self.open_order_id,
|
'open_order_id': self.open_order_id,
|
||||||
'filled_entry_orders': filled_entries,
|
'orders': orders,
|
||||||
'filled_exit_orders': filled_exits,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -453,7 +446,8 @@ class LocalTrade():
|
|||||||
new_loss = float(current_price * (1 - abs(stoploss)))
|
new_loss = float(current_price * (1 - abs(stoploss)))
|
||||||
|
|
||||||
# no stop loss assigned yet
|
# no stop loss assigned yet
|
||||||
if not self.stop_loss:
|
# if not self.stop_loss:
|
||||||
|
if self.initial_stop_loss_pct is None:
|
||||||
logger.debug(f"{self.pair} - Assigning new stoploss...")
|
logger.debug(f"{self.pair} - Assigning new stoploss...")
|
||||||
self._set_new_stoploss(new_loss, stoploss)
|
self._set_new_stoploss(new_loss, stoploss)
|
||||||
self.initial_stop_loss = new_loss
|
self.initial_stop_loss = new_loss
|
||||||
@ -793,6 +787,7 @@ class LocalTrade():
|
|||||||
logger.info(f"Stoploss for {trade} needs adjustment...")
|
logger.info(f"Stoploss for {trade} needs adjustment...")
|
||||||
# Force reset of stoploss
|
# Force reset of stoploss
|
||||||
trade.stop_loss = None
|
trade.stop_loss = None
|
||||||
|
trade.initial_stop_loss_pct = None
|
||||||
trade.adjust_stop_loss(trade.open_rate, desired_stoploss)
|
trade.adjust_stop_loss(trade.open_rate, desired_stoploss)
|
||||||
logger.info(f"New stoploss: {trade.stop_loss}.")
|
logger.info(f"New stoploss: {trade.stop_loss}.")
|
||||||
|
|
||||||
|
@ -98,7 +98,7 @@ class AgeFilter(IPairList):
|
|||||||
"""
|
"""
|
||||||
Validate age for the ticker
|
Validate age for the ticker
|
||||||
:param pair: Pair that's currently validated
|
:param pair: Pair that's currently validated
|
||||||
:param ticker: ticker dict as returned from ccxt.fetch_tickers()
|
:param daily_candles: Downloaded daily candles
|
||||||
:return: True if the pair can stay, false if it should be removed
|
:return: True if the pair can stay, false if it should be removed
|
||||||
"""
|
"""
|
||||||
# Check symbol in cache
|
# Check symbol in cache
|
||||||
|
@ -51,7 +51,7 @@ class PrecisionFilter(IPairList):
|
|||||||
:param ticker: ticker dict as returned from ccxt.fetch_tickers()
|
:param ticker: ticker dict as returned from ccxt.fetch_tickers()
|
||||||
:return: True if the pair can stay, false if it should be removed
|
:return: True if the pair can stay, false if it should be removed
|
||||||
"""
|
"""
|
||||||
stop_price = ticker['ask'] * self._stoploss
|
stop_price = ticker['last'] * self._stoploss
|
||||||
|
|
||||||
# Adjust stop-prices to precision
|
# Adjust stop-prices to precision
|
||||||
sp = self._exchange.price_to_precision(pair, stop_price)
|
sp = self._exchange.price_to_precision(pair, stop_price)
|
||||||
|
@ -4,6 +4,7 @@ Spread pair list filter
|
|||||||
import logging
|
import logging
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.plugins.pairlist.IPairList import IPairList
|
from freqtrade.plugins.pairlist.IPairList import IPairList
|
||||||
|
|
||||||
|
|
||||||
@ -20,6 +21,12 @@ class SpreadFilter(IPairList):
|
|||||||
self._max_spread_ratio = pairlistconfig.get('max_spread_ratio', 0.005)
|
self._max_spread_ratio = pairlistconfig.get('max_spread_ratio', 0.005)
|
||||||
self._enabled = self._max_spread_ratio != 0
|
self._enabled = self._max_spread_ratio != 0
|
||||||
|
|
||||||
|
if not self._exchange.exchange_has('fetchTickers'):
|
||||||
|
raise OperationalException(
|
||||||
|
'Exchange does not support fetchTickers, therefore SpreadFilter cannot be used.'
|
||||||
|
'Please edit your config and restart the bot.'
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def needstickers(self) -> bool:
|
def needstickers(self) -> bool:
|
||||||
"""
|
"""
|
||||||
|
@ -90,7 +90,7 @@ class VolatilityFilter(IPairList):
|
|||||||
"""
|
"""
|
||||||
Validate trading range
|
Validate trading range
|
||||||
:param pair: Pair that's currently validated
|
:param pair: Pair that's currently validated
|
||||||
:param ticker: ticker dict as returned from ccxt.fetch_tickers()
|
:param daily_candles: Downloaded daily candles
|
||||||
:return: True if the pair can stay, false if it should be removed
|
:return: True if the pair can stay, false if it should be removed
|
||||||
"""
|
"""
|
||||||
# Check symbol in cache
|
# Check symbol in cache
|
||||||
|
@ -88,7 +88,7 @@ class RangeStabilityFilter(IPairList):
|
|||||||
"""
|
"""
|
||||||
Validate trading range
|
Validate trading range
|
||||||
:param pair: Pair that's currently validated
|
:param pair: Pair that's currently validated
|
||||||
:param ticker: ticker dict as returned from ccxt.fetch_tickers()
|
:param daily_candles: Downloaded daily candles
|
||||||
:return: True if the pair can stay, false if it should be removed
|
:return: True if the pair can stay, false if it should be removed
|
||||||
"""
|
"""
|
||||||
# Check symbol in cache
|
# Check symbol in cache
|
||||||
|
@ -44,7 +44,6 @@ class HyperOptLossResolver(IResolver):
|
|||||||
extra_dir=config.get('hyperopt_path'))
|
extra_dir=config.get('hyperopt_path'))
|
||||||
|
|
||||||
# Assign timeframe to be used in hyperopt
|
# Assign timeframe to be used in hyperopt
|
||||||
hyperoptloss.__class__.ticker_interval = str(config['timeframe'])
|
|
||||||
hyperoptloss.__class__.timeframe = str(config['timeframe'])
|
hyperoptloss.__class__.timeframe = str(config['timeframe'])
|
||||||
|
|
||||||
return hyperoptloss
|
return hyperoptloss
|
||||||
|
@ -6,6 +6,7 @@ This module load custom objects
|
|||||||
import importlib.util
|
import importlib.util
|
||||||
import inspect
|
import inspect
|
||||||
import logging
|
import logging
|
||||||
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, Iterator, List, Optional, Tuple, Type, Union
|
from typing import Any, Dict, Iterator, List, Optional, Tuple, Type, Union
|
||||||
|
|
||||||
@ -15,6 +16,22 @@ from freqtrade.exceptions import OperationalException
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class PathModifier:
|
||||||
|
def __init__(self, path: Path):
|
||||||
|
self.path = path
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
"""Inject path to allow importing with relative imports."""
|
||||||
|
sys.path.insert(0, str(self.path))
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
"""Undo insertion of local path."""
|
||||||
|
str_path = str(self.path)
|
||||||
|
if str_path in sys.path:
|
||||||
|
sys.path.remove(str_path)
|
||||||
|
|
||||||
|
|
||||||
class IResolver:
|
class IResolver:
|
||||||
"""
|
"""
|
||||||
This class contains all the logic to load custom classes
|
This class contains all the logic to load custom classes
|
||||||
@ -57,7 +74,9 @@ class IResolver:
|
|||||||
|
|
||||||
# Generate spec based on absolute path
|
# Generate spec based on absolute path
|
||||||
# Pass object_name as first argument to have logging print a reasonable name.
|
# Pass object_name as first argument to have logging print a reasonable name.
|
||||||
spec = importlib.util.spec_from_file_location(object_name or "", str(module_path))
|
with PathModifier(module_path.parent):
|
||||||
|
module_name = module_path.stem or ""
|
||||||
|
spec = importlib.util.spec_from_file_location(module_name, str(module_path))
|
||||||
if not spec:
|
if not spec:
|
||||||
return iter([None])
|
return iter([None])
|
||||||
|
|
||||||
@ -75,8 +94,11 @@ class IResolver:
|
|||||||
name, obj in inspect.getmembers(
|
name, obj in inspect.getmembers(
|
||||||
module, inspect.isclass) if ((object_name is None or object_name == name)
|
module, inspect.isclass) if ((object_name is None or object_name == name)
|
||||||
and issubclass(obj, cls.object_type)
|
and issubclass(obj, cls.object_type)
|
||||||
and obj is not cls.object_type)
|
and obj is not cls.object_type
|
||||||
|
and obj.__module__ == module_name
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
# The __module__ check ensures we only use strategies that are defined in this folder.
|
||||||
return valid_objects_gen
|
return valid_objects_gen
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -45,14 +45,6 @@ class StrategyResolver(IResolver):
|
|||||||
strategy_name, config=config,
|
strategy_name, config=config,
|
||||||
extra_dir=config.get('strategy_path'))
|
extra_dir=config.get('strategy_path'))
|
||||||
|
|
||||||
if hasattr(strategy, 'ticker_interval') and not hasattr(strategy, 'timeframe'):
|
|
||||||
# Assign ticker_interval to timeframe to keep compatibility
|
|
||||||
if 'timeframe' not in config:
|
|
||||||
logger.warning(
|
|
||||||
"DEPRECATED: Please migrate to using 'timeframe' instead of 'ticker_interval'."
|
|
||||||
)
|
|
||||||
strategy.timeframe = strategy.ticker_interval
|
|
||||||
|
|
||||||
if strategy._ft_params_from_file:
|
if strategy._ft_params_from_file:
|
||||||
# Set parameters from Hyperopt results file
|
# Set parameters from Hyperopt results file
|
||||||
params = strategy._ft_params_from_file
|
params = strategy._ft_params_from_file
|
||||||
@ -145,10 +137,6 @@ class StrategyResolver(IResolver):
|
|||||||
"""
|
"""
|
||||||
Normalize attributes to have the correct type.
|
Normalize attributes to have the correct type.
|
||||||
"""
|
"""
|
||||||
# Assign deprecated variable - to not break users code relying on this.
|
|
||||||
if hasattr(strategy, 'timeframe'):
|
|
||||||
strategy.ticker_interval = strategy.timeframe
|
|
||||||
|
|
||||||
# Sort and apply type conversions
|
# Sort and apply type conversions
|
||||||
if hasattr(strategy, 'minimal_roi'):
|
if hasattr(strategy, 'minimal_roi'):
|
||||||
strategy.minimal_roi = dict(sorted(
|
strategy.minimal_roi = dict(sorted(
|
||||||
|
@ -177,6 +177,22 @@ class ShowConfig(BaseModel):
|
|||||||
max_entry_position_adjustment: int
|
max_entry_position_adjustment: int
|
||||||
|
|
||||||
|
|
||||||
|
class OrderSchema(BaseModel):
|
||||||
|
pair: str
|
||||||
|
order_id: str
|
||||||
|
status: str
|
||||||
|
remaining: float
|
||||||
|
amount: float
|
||||||
|
safe_price: float
|
||||||
|
cost: float
|
||||||
|
filled: float
|
||||||
|
ft_order_side: str
|
||||||
|
order_type: str
|
||||||
|
is_open: bool
|
||||||
|
order_timestamp: Optional[int]
|
||||||
|
order_filled_timestamp: Optional[int]
|
||||||
|
|
||||||
|
|
||||||
class TradeSchema(BaseModel):
|
class TradeSchema(BaseModel):
|
||||||
trade_id: int
|
trade_id: int
|
||||||
pair: str
|
pair: str
|
||||||
@ -224,6 +240,7 @@ class TradeSchema(BaseModel):
|
|||||||
min_rate: Optional[float]
|
min_rate: Optional[float]
|
||||||
max_rate: Optional[float]
|
max_rate: Optional[float]
|
||||||
open_order_id: Optional[str]
|
open_order_id: Optional[str]
|
||||||
|
orders: List[OrderSchema]
|
||||||
|
|
||||||
|
|
||||||
class OpenTradeSchema(TradeSchema):
|
class OpenTradeSchema(TradeSchema):
|
||||||
|
@ -32,7 +32,8 @@ logger = logging.getLogger(__name__)
|
|||||||
# 1.11: forcebuy and forcesell accept ordertype
|
# 1.11: forcebuy and forcesell accept ordertype
|
||||||
# 1.12: add blacklist delete endpoint
|
# 1.12: add blacklist delete endpoint
|
||||||
# 1.13: forcebuy supports stake_amount
|
# 1.13: forcebuy supports stake_amount
|
||||||
API_VERSION = 1.13
|
# 1.14: Add entry/exit orders to trade response
|
||||||
|
API_VERSION = 1.14
|
||||||
|
|
||||||
# Public API, requires no auth.
|
# Public API, requires no auth.
|
||||||
router_public = APIRouter()
|
router_public = APIRouter()
|
||||||
@ -136,7 +137,7 @@ def show_config(rpc: Optional[RPC] = Depends(get_rpc_optional), config=Depends(g
|
|||||||
def forcebuy(payload: ForceBuyPayload, rpc: RPC = Depends(get_rpc)):
|
def forcebuy(payload: ForceBuyPayload, rpc: RPC = Depends(get_rpc)):
|
||||||
ordertype = payload.ordertype.value if payload.ordertype else None
|
ordertype = payload.ordertype.value if payload.ordertype else None
|
||||||
stake_amount = payload.stakeamount if payload.stakeamount else None
|
stake_amount = payload.stakeamount if payload.stakeamount else None
|
||||||
entry_tag = payload.entry_tag if payload.entry_tag else None
|
entry_tag = payload.entry_tag if payload.entry_tag else 'forceentry'
|
||||||
|
|
||||||
trade = rpc._rpc_forcebuy(payload.pair, payload.price, ordertype, stake_amount, entry_tag)
|
trade = rpc._rpc_forcebuy(payload.pair, payload.price, ordertype, stake_amount, entry_tag)
|
||||||
|
|
||||||
|
@ -582,7 +582,7 @@ class RPC:
|
|||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
pair = self._freqtrade.exchange.get_valid_pair_combination(coin, stake_currency)
|
pair = self._freqtrade.exchange.get_valid_pair_combination(coin, stake_currency)
|
||||||
rate = tickers.get(pair, {}).get('bid', None)
|
rate = tickers.get(pair, {}).get('last', None)
|
||||||
if rate:
|
if rate:
|
||||||
if pair.startswith(stake_currency) and not pair.endswith(stake_currency):
|
if pair.startswith(stake_currency) and not pair.endswith(stake_currency):
|
||||||
rate = 1.0 / rate
|
rate = 1.0 / rate
|
||||||
@ -713,7 +713,7 @@ class RPC:
|
|||||||
|
|
||||||
def _rpc_forcebuy(self, pair: str, price: Optional[float], order_type: Optional[str] = None,
|
def _rpc_forcebuy(self, pair: str, price: Optional[float], order_type: Optional[str] = None,
|
||||||
stake_amount: Optional[float] = None,
|
stake_amount: Optional[float] = None,
|
||||||
buy_tag: Optional[str] = None) -> Optional[Trade]:
|
buy_tag: Optional[str] = 'forceentry') -> Optional[Trade]:
|
||||||
"""
|
"""
|
||||||
Handler for forcebuy <asset> <price>
|
Handler for forcebuy <asset> <price>
|
||||||
Buys a pair trade at the given or current price
|
Buys a pair trade at the given or current price
|
||||||
|
@ -379,6 +379,8 @@ class Telegram(RPCHandler):
|
|||||||
first_avg = filled_orders[0]["safe_price"]
|
first_avg = filled_orders[0]["safe_price"]
|
||||||
|
|
||||||
for x, order in enumerate(filled_orders):
|
for x, order in enumerate(filled_orders):
|
||||||
|
if order['ft_order_side'] != 'buy':
|
||||||
|
continue
|
||||||
cur_entry_datetime = arrow.get(order["order_filled_date"])
|
cur_entry_datetime = arrow.get(order["order_filled_date"])
|
||||||
cur_entry_amount = order["amount"]
|
cur_entry_amount = order["amount"]
|
||||||
cur_entry_average = order["safe_price"]
|
cur_entry_average = order["safe_price"]
|
||||||
@ -444,7 +446,7 @@ class Telegram(RPCHandler):
|
|||||||
messages = []
|
messages = []
|
||||||
for r in results:
|
for r in results:
|
||||||
r['open_date_hum'] = arrow.get(r['open_date']).humanize()
|
r['open_date_hum'] = arrow.get(r['open_date']).humanize()
|
||||||
r['num_entries'] = len(r['filled_entry_orders'])
|
r['num_entries'] = len([o for o in r['orders'] if o['ft_order_side'] == 'buy'])
|
||||||
r['sell_reason'] = r.get('sell_reason', "")
|
r['sell_reason'] = r.get('sell_reason', "")
|
||||||
lines = [
|
lines = [
|
||||||
"*Trade ID:* `{trade_id}`" +
|
"*Trade ID:* `{trade_id}`" +
|
||||||
@ -488,8 +490,8 @@ class Telegram(RPCHandler):
|
|||||||
lines.append("*Open Order:* `{open_order}`")
|
lines.append("*Open Order:* `{open_order}`")
|
||||||
|
|
||||||
lines_detail = self._prepare_entry_details(
|
lines_detail = self._prepare_entry_details(
|
||||||
r['filled_entry_orders'], r['base_currency'], r['is_open'])
|
r['orders'], r['base_currency'], r['is_open'])
|
||||||
lines.extend((lines_detail if (len(r['filled_entry_orders']) > 1) else ""))
|
lines.extend(lines_detail if lines_detail else "")
|
||||||
|
|
||||||
# Filter empty lines using list-comprehension
|
# Filter empty lines using list-comprehension
|
||||||
messages.append("\n".join([line for line in lines if line]).format(**r))
|
messages.append("\n".join([line for line in lines if line]).format(**r))
|
||||||
|
@ -55,7 +55,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
Attributes you can use:
|
Attributes you can use:
|
||||||
minimal_roi -> Dict: Minimal ROI designed for the strategy
|
minimal_roi -> Dict: Minimal ROI designed for the strategy
|
||||||
stoploss -> float: optimal stoploss designed for the strategy
|
stoploss -> float: optimal stoploss designed for the strategy
|
||||||
timeframe -> str: value of the timeframe (ticker interval) to use with the strategy
|
timeframe -> str: value of the timeframe to use with the strategy
|
||||||
"""
|
"""
|
||||||
# Strategy interface version
|
# Strategy interface version
|
||||||
# Default to version 2
|
# Default to version 2
|
||||||
@ -81,7 +81,6 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
use_custom_stoploss: bool = False
|
use_custom_stoploss: bool = False
|
||||||
|
|
||||||
# associated timeframe
|
# associated timeframe
|
||||||
ticker_interval: str # DEPRECATED
|
|
||||||
timeframe: str
|
timeframe: str
|
||||||
|
|
||||||
# Optional order types
|
# Optional order types
|
||||||
|
12
freqtrade/templates/subtemplates/exchange_huobi.j2
Normal file
12
freqtrade/templates/subtemplates/exchange_huobi.j2
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
"exchange": {
|
||||||
|
"name": "{{ exchange_name | lower }}",
|
||||||
|
"key": "{{ exchange_key }}",
|
||||||
|
"secret": "{{ exchange_secret }}",
|
||||||
|
"ccxt_config": {},
|
||||||
|
"ccxt_async_config": {},
|
||||||
|
"pair_whitelist": [
|
||||||
|
],
|
||||||
|
"pair_blacklist": [
|
||||||
|
"HT/.*"
|
||||||
|
]
|
||||||
|
}
|
@ -6,9 +6,9 @@
|
|||||||
coveralls==3.3.1
|
coveralls==3.3.1
|
||||||
flake8==4.0.1
|
flake8==4.0.1
|
||||||
flake8-tidy-imports==4.6.0
|
flake8-tidy-imports==4.6.0
|
||||||
mypy==0.931
|
mypy==0.941
|
||||||
pytest==7.0.1
|
pytest==7.1.1
|
||||||
pytest-asyncio==0.18.1
|
pytest-asyncio==0.18.2
|
||||||
pytest-cov==3.0.0
|
pytest-cov==3.0.0
|
||||||
pytest-mock==3.7.0
|
pytest-mock==3.7.0
|
||||||
pytest-random-order==1.0.4
|
pytest-random-order==1.0.4
|
||||||
@ -17,13 +17,13 @@ isort==5.10.1
|
|||||||
time-machine==2.6.0
|
time-machine==2.6.0
|
||||||
|
|
||||||
# Convert jupyter notebooks to markdown documents
|
# Convert jupyter notebooks to markdown documents
|
||||||
nbconvert==6.4.2
|
nbconvert==6.4.4
|
||||||
|
|
||||||
# mypy types
|
# mypy types
|
||||||
types-cachetools==4.2.9
|
types-cachetools==5.0.0
|
||||||
types-filelock==3.2.5
|
types-filelock==3.2.5
|
||||||
types-requests==2.27.10
|
types-requests==2.27.14
|
||||||
types-tabulate==0.8.5
|
types-tabulate==0.8.6
|
||||||
|
|
||||||
# Extensions to datetime library
|
# Extensions to datetime library
|
||||||
types-python-dateutil==2.8.9
|
types-python-dateutil==2.8.10
|
@ -1,17 +1,17 @@
|
|||||||
numpy==1.22.2
|
numpy==1.22.3
|
||||||
pandas==1.4.1
|
pandas==1.4.1
|
||||||
pandas-ta==0.3.14b
|
pandas-ta==0.3.14b
|
||||||
|
|
||||||
ccxt==1.73.70
|
ccxt==1.76.65
|
||||||
# Pin cryptography for now due to rust build errors with piwheels
|
# Pin cryptography for now due to rust build errors with piwheels
|
||||||
cryptography==36.0.1
|
cryptography==36.0.2
|
||||||
aiohttp==3.8.1
|
aiohttp==3.8.1
|
||||||
SQLAlchemy==1.4.31
|
SQLAlchemy==1.4.32
|
||||||
python-telegram-bot==13.11
|
python-telegram-bot==13.11
|
||||||
arrow==1.2.2
|
arrow==1.2.2
|
||||||
cachetools==4.2.2
|
cachetools==4.2.2
|
||||||
requests==2.27.1
|
requests==2.27.1
|
||||||
urllib3==1.26.8
|
urllib3==1.26.9
|
||||||
jsonschema==4.4.0
|
jsonschema==4.4.0
|
||||||
TA-Lib==0.4.24
|
TA-Lib==0.4.24
|
||||||
technical==1.3.0
|
technical==1.3.0
|
||||||
@ -31,8 +31,8 @@ python-rapidjson==1.6
|
|||||||
sdnotify==0.3.2
|
sdnotify==0.3.2
|
||||||
|
|
||||||
# API Server
|
# API Server
|
||||||
fastapi==0.74.0
|
fastapi==0.75.0
|
||||||
uvicorn==0.17.5
|
uvicorn==0.17.6
|
||||||
pyjwt==2.3.0
|
pyjwt==2.3.0
|
||||||
aiofiles==0.8.0
|
aiofiles==0.8.0
|
||||||
psutil==5.9.0
|
psutil==5.9.0
|
||||||
|
2
setup.py
2
setup.py
@ -42,7 +42,7 @@ setup(
|
|||||||
],
|
],
|
||||||
install_requires=[
|
install_requires=[
|
||||||
# from requirements.txt
|
# from requirements.txt
|
||||||
'ccxt>=1.66.32',
|
'ccxt>=1.76.5',
|
||||||
'SQLAlchemy',
|
'SQLAlchemy',
|
||||||
'python-telegram-bot>=13.4',
|
'python-telegram-bot>=13.4',
|
||||||
'arrow>=0.17.0',
|
'arrow>=0.17.0',
|
||||||
|
3
setup.sh
3
setup.sh
@ -132,6 +132,9 @@ function install_macos() {
|
|||||||
echo_block "Installing Brew"
|
echo_block "Installing Brew"
|
||||||
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
|
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
brew install gettext
|
||||||
|
|
||||||
#Gets number after decimal in python version
|
#Gets number after decimal in python version
|
||||||
version=$(egrep -o 3.\[0-9\]+ <<< $PYTHON | sed 's/3.//g')
|
version=$(egrep -o 3.\[0-9\]+ <<< $PYTHON | sed 's/3.//g')
|
||||||
|
|
||||||
|
@ -107,6 +107,8 @@ def patch_exchange(mocker, api_mock=None, id='binance', mock_markets=True) -> No
|
|||||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||||
else:
|
else:
|
||||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock())
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.timeframes', PropertyMock(
|
||||||
|
return_value=['5m', '15m', '1h', '1d']))
|
||||||
|
|
||||||
|
|
||||||
def get_patched_exchange(mocker, config, api_mock=None, id='binance',
|
def get_patched_exchange(mocker, config, api_mock=None, id='binance',
|
||||||
@ -1017,8 +1019,8 @@ def limit_buy_order_open():
|
|||||||
'type': 'limit',
|
'type': 'limit',
|
||||||
'side': 'buy',
|
'side': 'buy',
|
||||||
'symbol': 'mocked',
|
'symbol': 'mocked',
|
||||||
|
'timestamp': arrow.utcnow().int_timestamp * 1000,
|
||||||
'datetime': arrow.utcnow().isoformat(),
|
'datetime': arrow.utcnow().isoformat(),
|
||||||
'timestamp': arrow.utcnow().int_timestamp,
|
|
||||||
'price': 0.00001099,
|
'price': 0.00001099,
|
||||||
'amount': 90.99181073,
|
'amount': 90.99181073,
|
||||||
'filled': 0.0,
|
'filled': 0.0,
|
||||||
@ -1044,6 +1046,7 @@ def market_buy_order():
|
|||||||
'type': 'market',
|
'type': 'market',
|
||||||
'side': 'buy',
|
'side': 'buy',
|
||||||
'symbol': 'mocked',
|
'symbol': 'mocked',
|
||||||
|
'timestamp': arrow.utcnow().int_timestamp * 1000,
|
||||||
'datetime': arrow.utcnow().isoformat(),
|
'datetime': arrow.utcnow().isoformat(),
|
||||||
'price': 0.00004099,
|
'price': 0.00004099,
|
||||||
'amount': 91.99181073,
|
'amount': 91.99181073,
|
||||||
@ -1060,6 +1063,7 @@ def market_sell_order():
|
|||||||
'type': 'market',
|
'type': 'market',
|
||||||
'side': 'sell',
|
'side': 'sell',
|
||||||
'symbol': 'mocked',
|
'symbol': 'mocked',
|
||||||
|
'timestamp': arrow.utcnow().int_timestamp * 1000,
|
||||||
'datetime': arrow.utcnow().isoformat(),
|
'datetime': arrow.utcnow().isoformat(),
|
||||||
'price': 0.00004173,
|
'price': 0.00004173,
|
||||||
'amount': 91.99181073,
|
'amount': 91.99181073,
|
||||||
@ -1076,7 +1080,8 @@ def limit_buy_order_old():
|
|||||||
'type': 'limit',
|
'type': 'limit',
|
||||||
'side': 'buy',
|
'side': 'buy',
|
||||||
'symbol': 'mocked',
|
'symbol': 'mocked',
|
||||||
'datetime': str(arrow.utcnow().shift(minutes=-601).datetime),
|
'datetime': arrow.utcnow().shift(minutes=-601).isoformat(),
|
||||||
|
'timestamp': arrow.utcnow().shift(minutes=-601).int_timestamp * 1000,
|
||||||
'price': 0.00001099,
|
'price': 0.00001099,
|
||||||
'amount': 90.99181073,
|
'amount': 90.99181073,
|
||||||
'filled': 0.0,
|
'filled': 0.0,
|
||||||
@ -1092,6 +1097,7 @@ def limit_sell_order_old():
|
|||||||
'type': 'limit',
|
'type': 'limit',
|
||||||
'side': 'sell',
|
'side': 'sell',
|
||||||
'symbol': 'ETH/BTC',
|
'symbol': 'ETH/BTC',
|
||||||
|
'timestamp': arrow.utcnow().shift(minutes=-601).int_timestamp * 1000,
|
||||||
'datetime': arrow.utcnow().shift(minutes=-601).isoformat(),
|
'datetime': arrow.utcnow().shift(minutes=-601).isoformat(),
|
||||||
'price': 0.00001099,
|
'price': 0.00001099,
|
||||||
'amount': 90.99181073,
|
'amount': 90.99181073,
|
||||||
@ -1108,6 +1114,7 @@ def limit_buy_order_old_partial():
|
|||||||
'type': 'limit',
|
'type': 'limit',
|
||||||
'side': 'buy',
|
'side': 'buy',
|
||||||
'symbol': 'ETH/BTC',
|
'symbol': 'ETH/BTC',
|
||||||
|
'timestamp': arrow.utcnow().shift(minutes=-601).int_timestamp * 1000,
|
||||||
'datetime': arrow.utcnow().shift(minutes=-601).isoformat(),
|
'datetime': arrow.utcnow().shift(minutes=-601).isoformat(),
|
||||||
'price': 0.00001099,
|
'price': 0.00001099,
|
||||||
'amount': 90.99181073,
|
'amount': 90.99181073,
|
||||||
@ -1137,7 +1144,7 @@ def limit_buy_order_canceled_empty(request):
|
|||||||
'info': {},
|
'info': {},
|
||||||
'id': '1234512345',
|
'id': '1234512345',
|
||||||
'clientOrderId': None,
|
'clientOrderId': None,
|
||||||
'timestamp': arrow.utcnow().shift(minutes=-601).int_timestamp,
|
'timestamp': arrow.utcnow().shift(minutes=-601).int_timestamp * 1000,
|
||||||
'datetime': arrow.utcnow().shift(minutes=-601).isoformat(),
|
'datetime': arrow.utcnow().shift(minutes=-601).isoformat(),
|
||||||
'lastTradeTimestamp': None,
|
'lastTradeTimestamp': None,
|
||||||
'symbol': 'LTC/USDT',
|
'symbol': 'LTC/USDT',
|
||||||
@ -1158,7 +1165,7 @@ def limit_buy_order_canceled_empty(request):
|
|||||||
'info': {},
|
'info': {},
|
||||||
'id': 'AZNPFF-4AC4N-7MKTAT',
|
'id': 'AZNPFF-4AC4N-7MKTAT',
|
||||||
'clientOrderId': None,
|
'clientOrderId': None,
|
||||||
'timestamp': arrow.utcnow().shift(minutes=-601).int_timestamp,
|
'timestamp': arrow.utcnow().shift(minutes=-601).int_timestamp * 1000,
|
||||||
'datetime': arrow.utcnow().shift(minutes=-601).isoformat(),
|
'datetime': arrow.utcnow().shift(minutes=-601).isoformat(),
|
||||||
'lastTradeTimestamp': None,
|
'lastTradeTimestamp': None,
|
||||||
'status': 'canceled',
|
'status': 'canceled',
|
||||||
@ -1179,7 +1186,7 @@ def limit_buy_order_canceled_empty(request):
|
|||||||
'info': {},
|
'info': {},
|
||||||
'id': '1234512345',
|
'id': '1234512345',
|
||||||
'clientOrderId': 'alb1234123',
|
'clientOrderId': 'alb1234123',
|
||||||
'timestamp': arrow.utcnow().shift(minutes=-601).int_timestamp,
|
'timestamp': arrow.utcnow().shift(minutes=-601).int_timestamp * 1000,
|
||||||
'datetime': arrow.utcnow().shift(minutes=-601).isoformat(),
|
'datetime': arrow.utcnow().shift(minutes=-601).isoformat(),
|
||||||
'lastTradeTimestamp': None,
|
'lastTradeTimestamp': None,
|
||||||
'symbol': 'LTC/USDT',
|
'symbol': 'LTC/USDT',
|
||||||
@ -1200,7 +1207,7 @@ def limit_buy_order_canceled_empty(request):
|
|||||||
'info': {},
|
'info': {},
|
||||||
'id': '1234512345',
|
'id': '1234512345',
|
||||||
'clientOrderId': 'alb1234123',
|
'clientOrderId': 'alb1234123',
|
||||||
'timestamp': arrow.utcnow().shift(minutes=-601).int_timestamp,
|
'timestamp': arrow.utcnow().shift(minutes=-601).int_timestamp * 1000,
|
||||||
'datetime': arrow.utcnow().shift(minutes=-601).isoformat(),
|
'datetime': arrow.utcnow().shift(minutes=-601).isoformat(),
|
||||||
'lastTradeTimestamp': None,
|
'lastTradeTimestamp': None,
|
||||||
'symbol': 'LTC/USDT',
|
'symbol': 'LTC/USDT',
|
||||||
@ -1226,7 +1233,7 @@ def limit_sell_order_open():
|
|||||||
'side': 'sell',
|
'side': 'sell',
|
||||||
'symbol': 'mocked',
|
'symbol': 'mocked',
|
||||||
'datetime': arrow.utcnow().isoformat(),
|
'datetime': arrow.utcnow().isoformat(),
|
||||||
'timestamp': arrow.utcnow().int_timestamp,
|
'timestamp': arrow.utcnow().int_timestamp * 1000,
|
||||||
'price': 0.00001173,
|
'price': 0.00001173,
|
||||||
'amount': 90.99181073,
|
'amount': 90.99181073,
|
||||||
'filled': 0.0,
|
'filled': 0.0,
|
||||||
@ -1392,7 +1399,7 @@ def tickers():
|
|||||||
'BLK/BTC': {
|
'BLK/BTC': {
|
||||||
'symbol': 'BLK/BTC',
|
'symbol': 'BLK/BTC',
|
||||||
'timestamp': 1522014806072,
|
'timestamp': 1522014806072,
|
||||||
'datetime': '2018-03-25T21:53:26.720Z',
|
'datetime': '2018-03-25T21:53:26.072Z',
|
||||||
'high': 0.007745,
|
'high': 0.007745,
|
||||||
'low': 0.007512,
|
'low': 0.007512,
|
||||||
'bid': 0.007729,
|
'bid': 0.007729,
|
||||||
@ -1888,7 +1895,8 @@ def buy_order_fee():
|
|||||||
'type': 'limit',
|
'type': 'limit',
|
||||||
'side': 'buy',
|
'side': 'buy',
|
||||||
'symbol': 'mocked',
|
'symbol': 'mocked',
|
||||||
'datetime': str(arrow.utcnow().shift(minutes=-601).datetime),
|
'timestamp': arrow.utcnow().shift(minutes=-601).int_timestamp * 1000,
|
||||||
|
'datetime': arrow.utcnow().shift(minutes=-601).isoformat(),
|
||||||
'price': 0.245441,
|
'price': 0.245441,
|
||||||
'amount': 8.0,
|
'amount': 8.0,
|
||||||
'cost': 1.963528,
|
'cost': 1.963528,
|
||||||
@ -2186,7 +2194,7 @@ def limit_buy_order_usdt_open():
|
|||||||
'side': 'buy',
|
'side': 'buy',
|
||||||
'symbol': 'mocked',
|
'symbol': 'mocked',
|
||||||
'datetime': arrow.utcnow().isoformat(),
|
'datetime': arrow.utcnow().isoformat(),
|
||||||
'timestamp': arrow.utcnow().int_timestamp,
|
'timestamp': arrow.utcnow().int_timestamp * 1000,
|
||||||
'price': 2.00,
|
'price': 2.00,
|
||||||
'amount': 30.0,
|
'amount': 30.0,
|
||||||
'filled': 0.0,
|
'filled': 0.0,
|
||||||
@ -2213,7 +2221,7 @@ def limit_sell_order_usdt_open():
|
|||||||
'side': 'sell',
|
'side': 'sell',
|
||||||
'symbol': 'mocked',
|
'symbol': 'mocked',
|
||||||
'datetime': arrow.utcnow().isoformat(),
|
'datetime': arrow.utcnow().isoformat(),
|
||||||
'timestamp': arrow.utcnow().int_timestamp,
|
'timestamp': arrow.utcnow().int_timestamp * 1000,
|
||||||
'price': 2.20,
|
'price': 2.20,
|
||||||
'amount': 30.0,
|
'amount': 30.0,
|
||||||
'filled': 0.0,
|
'filled': 0.0,
|
||||||
@ -2238,6 +2246,7 @@ def market_buy_order_usdt():
|
|||||||
'type': 'market',
|
'type': 'market',
|
||||||
'side': 'buy',
|
'side': 'buy',
|
||||||
'symbol': 'mocked',
|
'symbol': 'mocked',
|
||||||
|
'timestamp': arrow.utcnow().int_timestamp * 1000,
|
||||||
'datetime': arrow.utcnow().isoformat(),
|
'datetime': arrow.utcnow().isoformat(),
|
||||||
'price': 2.00,
|
'price': 2.00,
|
||||||
'amount': 30.0,
|
'amount': 30.0,
|
||||||
@ -2294,6 +2303,7 @@ def market_sell_order_usdt():
|
|||||||
'type': 'market',
|
'type': 'market',
|
||||||
'side': 'sell',
|
'side': 'sell',
|
||||||
'symbol': 'mocked',
|
'symbol': 'mocked',
|
||||||
|
'timestamp': arrow.utcnow().int_timestamp * 1000,
|
||||||
'datetime': arrow.utcnow().isoformat(),
|
'datetime': arrow.utcnow().isoformat(),
|
||||||
'price': 2.20,
|
'price': 2.20,
|
||||||
'amount': 30.0,
|
'amount': 30.0,
|
||||||
|
@ -59,6 +59,12 @@ EXCHANGES = {
|
|||||||
'hasQuoteVolume': True,
|
'hasQuoteVolume': True,
|
||||||
'timeframe': '5m',
|
'timeframe': '5m',
|
||||||
},
|
},
|
||||||
|
'huobi': {
|
||||||
|
'pair': 'BTC/USDT',
|
||||||
|
'stake_currency': 'USDT',
|
||||||
|
'hasQuoteVolume': True,
|
||||||
|
'timeframe': '5m',
|
||||||
|
},
|
||||||
'bitvavo': {
|
'bitvavo': {
|
||||||
'pair': 'BTC/EUR',
|
'pair': 'BTC/EUR',
|
||||||
'stake_currency': 'EUR',
|
'stake_currency': 'EUR',
|
||||||
@ -140,7 +146,10 @@ class TestCCXTExchange():
|
|||||||
else:
|
else:
|
||||||
next_limit = exchange.get_next_limit_in_list(
|
next_limit = exchange.get_next_limit_in_list(
|
||||||
val, l2_limit_range, l2_limit_range_required)
|
val, l2_limit_range, l2_limit_range_required)
|
||||||
if next_limit is None or next_limit > 200:
|
if next_limit is None:
|
||||||
|
assert len(l2['asks']) > 100
|
||||||
|
assert len(l2['asks']) > 100
|
||||||
|
elif next_limit > 200:
|
||||||
# Large orderbook sizes can be a problem for some exchanges (bitrex ...)
|
# Large orderbook sizes can be a problem for some exchanges (bitrex ...)
|
||||||
assert len(l2['asks']) > 200
|
assert len(l2['asks']) > 200
|
||||||
assert len(l2['asks']) > 200
|
assert len(l2['asks']) > 200
|
||||||
|
@ -166,7 +166,7 @@ def test_exchange_resolver(default_conf, mocker, caplog):
|
|||||||
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
|
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
|
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
|
||||||
|
|
||||||
exchange = ExchangeResolver.load_exchange('huobi', default_conf)
|
exchange = ExchangeResolver.load_exchange('zaif', default_conf)
|
||||||
assert isinstance(exchange, Exchange)
|
assert isinstance(exchange, Exchange)
|
||||||
assert log_has_re(r"No .* specific subclass found. Using the generic class instead.", caplog)
|
assert log_has_re(r"No .* specific subclass found. Using the generic class instead.", caplog)
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
@ -1692,6 +1692,13 @@ def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None:
|
|||||||
cache=False)
|
cache=False)
|
||||||
assert len(res) == 3
|
assert len(res) == 3
|
||||||
assert exchange._api_async.fetch_ohlcv.call_count == 3
|
assert exchange._api_async.fetch_ohlcv.call_count == 3
|
||||||
|
exchange._api_async.fetch_ohlcv.reset_mock()
|
||||||
|
caplog.clear()
|
||||||
|
# Call with invalid timeframe
|
||||||
|
res = exchange.refresh_latest_ohlcv([('IOTA/ETH', '3m')], cache=False)
|
||||||
|
assert not res
|
||||||
|
assert len(res) == 0
|
||||||
|
assert log_has_re(r'Cannot download \(IOTA\/ETH, 3m\).*', caplog)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.exchange import Gateio
|
from freqtrade.exchange import Gateio
|
||||||
from freqtrade.resolvers.exchange_resolver import ExchangeResolver
|
from freqtrade.resolvers.exchange_resolver import ExchangeResolver
|
||||||
|
from tests.conftest import get_patched_exchange
|
||||||
|
|
||||||
|
|
||||||
def test_validate_order_types_gateio(default_conf, mocker):
|
def test_validate_order_types_gateio(default_conf, mocker):
|
||||||
@ -26,3 +29,39 @@ def test_validate_order_types_gateio(default_conf, mocker):
|
|||||||
with pytest.raises(OperationalException,
|
with pytest.raises(OperationalException,
|
||||||
match=r'Exchange .* does not support market orders.'):
|
match=r'Exchange .* does not support market orders.'):
|
||||||
ExchangeResolver.load_exchange('gateio', default_conf, True)
|
ExchangeResolver.load_exchange('gateio', default_conf, True)
|
||||||
|
|
||||||
|
|
||||||
|
def test_fetch_stoploss_order_gateio(default_conf, mocker):
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, id='gateio')
|
||||||
|
|
||||||
|
fetch_order_mock = MagicMock()
|
||||||
|
exchange.fetch_order = fetch_order_mock
|
||||||
|
|
||||||
|
exchange.fetch_stoploss_order('1234', 'ETH/BTC')
|
||||||
|
assert fetch_order_mock.call_count == 1
|
||||||
|
assert fetch_order_mock.call_args_list[0][1]['order_id'] == '1234'
|
||||||
|
assert fetch_order_mock.call_args_list[0][1]['pair'] == 'ETH/BTC'
|
||||||
|
assert fetch_order_mock.call_args_list[0][1]['params'] == {'stop': True}
|
||||||
|
|
||||||
|
|
||||||
|
def test_cancel_stoploss_order_gateio(default_conf, mocker):
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, id='gateio')
|
||||||
|
|
||||||
|
cancel_order_mock = MagicMock()
|
||||||
|
exchange.cancel_order = cancel_order_mock
|
||||||
|
|
||||||
|
exchange.cancel_stoploss_order('1234', 'ETH/BTC')
|
||||||
|
assert cancel_order_mock.call_count == 1
|
||||||
|
assert cancel_order_mock.call_args_list[0][1]['order_id'] == '1234'
|
||||||
|
assert cancel_order_mock.call_args_list[0][1]['pair'] == 'ETH/BTC'
|
||||||
|
assert cancel_order_mock.call_args_list[0][1]['params'] == {'stop': True}
|
||||||
|
|
||||||
|
|
||||||
|
def test_stoploss_adjust_gateio(mocker, default_conf):
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, id='gateio')
|
||||||
|
order = {
|
||||||
|
'price': 1500,
|
||||||
|
'stopPrice': 1500,
|
||||||
|
}
|
||||||
|
assert exchange.stoploss_adjust(1501, order)
|
||||||
|
assert not exchange.stoploss_adjust(1499, order)
|
||||||
|
109
tests/exchange/test_huobi.py
Normal file
109
tests/exchange/test_huobi.py
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
from random import randint
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
import ccxt
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from freqtrade.exceptions import DependencyException, InvalidOrderException, OperationalException
|
||||||
|
from tests.conftest import get_patched_exchange
|
||||||
|
from tests.exchange.test_exchange import ccxt_exceptionhandlers
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('limitratio,expected', [
|
||||||
|
(None, 220 * 0.99),
|
||||||
|
(0.99, 220 * 0.99),
|
||||||
|
(0.98, 220 * 0.98),
|
||||||
|
])
|
||||||
|
def test_stoploss_order_huobi(default_conf, mocker, limitratio, expected):
|
||||||
|
api_mock = MagicMock()
|
||||||
|
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
|
||||||
|
order_type = 'stop-limit'
|
||||||
|
|
||||||
|
api_mock.create_order = MagicMock(return_value={
|
||||||
|
'id': order_id,
|
||||||
|
'info': {
|
||||||
|
'foo': 'bar'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
default_conf['dry_run'] = False
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
|
||||||
|
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'huobi')
|
||||||
|
|
||||||
|
with pytest.raises(OperationalException):
|
||||||
|
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190,
|
||||||
|
order_types={'stoploss_on_exchange_limit_ratio': 1.05})
|
||||||
|
|
||||||
|
api_mock.create_order.reset_mock()
|
||||||
|
order_types = {} if limitratio is None else {'stoploss_on_exchange_limit_ratio': limitratio}
|
||||||
|
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types=order_types)
|
||||||
|
|
||||||
|
assert 'id' in order
|
||||||
|
assert 'info' in order
|
||||||
|
assert order['id'] == order_id
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC'
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['type'] == order_type
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell'
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['amount'] == 1
|
||||||
|
# Price should be 1% below stopprice
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['price'] == expected
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['params'] == {"stopPrice": 220,
|
||||||
|
"operator": "lte",
|
||||||
|
}
|
||||||
|
|
||||||
|
# test exception handling
|
||||||
|
with pytest.raises(DependencyException):
|
||||||
|
api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance"))
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'huobi')
|
||||||
|
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||||
|
|
||||||
|
with pytest.raises(InvalidOrderException):
|
||||||
|
api_mock.create_order = MagicMock(
|
||||||
|
side_effect=ccxt.InvalidOrder("binance Order would trigger immediately."))
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
|
||||||
|
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||||
|
|
||||||
|
ccxt_exceptionhandlers(mocker, default_conf, api_mock, "huobi",
|
||||||
|
"stoploss", "create_order", retries=1,
|
||||||
|
pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||||
|
|
||||||
|
|
||||||
|
def test_stoploss_order_dry_run_huobi(default_conf, mocker):
|
||||||
|
api_mock = MagicMock()
|
||||||
|
order_type = 'stop-limit'
|
||||||
|
default_conf['dry_run'] = True
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
|
||||||
|
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'huobi')
|
||||||
|
|
||||||
|
with pytest.raises(OperationalException):
|
||||||
|
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190,
|
||||||
|
order_types={'stoploss_on_exchange_limit_ratio': 1.05})
|
||||||
|
|
||||||
|
api_mock.create_order.reset_mock()
|
||||||
|
|
||||||
|
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||||
|
|
||||||
|
assert 'id' in order
|
||||||
|
assert 'info' in order
|
||||||
|
assert 'type' in order
|
||||||
|
|
||||||
|
assert order['type'] == order_type
|
||||||
|
assert order['price'] == 220
|
||||||
|
assert order['amount'] == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_stoploss_adjust_huobi(mocker, default_conf):
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, id='huobi')
|
||||||
|
order = {
|
||||||
|
'type': 'stop',
|
||||||
|
'price': 1500,
|
||||||
|
'stopPrice': '1500',
|
||||||
|
}
|
||||||
|
assert exchange.stoploss_adjust(1501, order)
|
||||||
|
assert not exchange.stoploss_adjust(1499, order)
|
||||||
|
# Test with invalid order case
|
||||||
|
order['type'] = 'stop_loss'
|
||||||
|
assert not exchange.stoploss_adjust(1501, order)
|
120
tests/exchange/test_kucoin.py
Normal file
120
tests/exchange/test_kucoin.py
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
from random import randint
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
import ccxt
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from freqtrade.exceptions import DependencyException, InvalidOrderException, OperationalException
|
||||||
|
from tests.conftest import get_patched_exchange
|
||||||
|
from tests.exchange.test_exchange import ccxt_exceptionhandlers
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('order_type', ['market', 'limit'])
|
||||||
|
@pytest.mark.parametrize('limitratio,expected', [
|
||||||
|
(None, 220 * 0.99),
|
||||||
|
(0.99, 220 * 0.99),
|
||||||
|
(0.98, 220 * 0.98),
|
||||||
|
])
|
||||||
|
def test_stoploss_order_kucoin(default_conf, mocker, limitratio, expected, order_type):
|
||||||
|
api_mock = MagicMock()
|
||||||
|
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
|
||||||
|
|
||||||
|
api_mock.create_order = MagicMock(return_value={
|
||||||
|
'id': order_id,
|
||||||
|
'info': {
|
||||||
|
'foo': 'bar'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
default_conf['dry_run'] = False
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
|
||||||
|
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kucoin')
|
||||||
|
if order_type == 'limit':
|
||||||
|
with pytest.raises(OperationalException):
|
||||||
|
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190,
|
||||||
|
order_types={
|
||||||
|
'stoploss': order_type,
|
||||||
|
'stoploss_on_exchange_limit_ratio': 1.05})
|
||||||
|
|
||||||
|
api_mock.create_order.reset_mock()
|
||||||
|
order_types = {'stoploss': order_type}
|
||||||
|
if limitratio is not None:
|
||||||
|
order_types.update({'stoploss_on_exchange_limit_ratio': limitratio})
|
||||||
|
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types=order_types)
|
||||||
|
|
||||||
|
assert 'id' in order
|
||||||
|
assert 'info' in order
|
||||||
|
assert order['id'] == order_id
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC'
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['type'] == order_type
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell'
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['amount'] == 1
|
||||||
|
# Price should be 1% below stopprice
|
||||||
|
if order_type == 'limit':
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['price'] == expected
|
||||||
|
else:
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['price'] is None
|
||||||
|
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['params'] == {
|
||||||
|
'stopPrice': 220,
|
||||||
|
'stop': 'loss'
|
||||||
|
}
|
||||||
|
|
||||||
|
# test exception handling
|
||||||
|
with pytest.raises(DependencyException):
|
||||||
|
api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance"))
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kucoin')
|
||||||
|
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||||
|
|
||||||
|
with pytest.raises(InvalidOrderException):
|
||||||
|
api_mock.create_order = MagicMock(
|
||||||
|
side_effect=ccxt.InvalidOrder("kucoin Order would trigger immediately."))
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kucoin')
|
||||||
|
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||||
|
|
||||||
|
ccxt_exceptionhandlers(mocker, default_conf, api_mock, "kucoin",
|
||||||
|
"stoploss", "create_order", retries=1,
|
||||||
|
pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||||
|
|
||||||
|
|
||||||
|
def test_stoploss_order_dry_run_kucoin(default_conf, mocker):
|
||||||
|
api_mock = MagicMock()
|
||||||
|
order_type = 'market'
|
||||||
|
default_conf['dry_run'] = True
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
|
||||||
|
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kucoin')
|
||||||
|
|
||||||
|
with pytest.raises(OperationalException):
|
||||||
|
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190,
|
||||||
|
order_types={'stoploss': 'limit',
|
||||||
|
'stoploss_on_exchange_limit_ratio': 1.05})
|
||||||
|
|
||||||
|
api_mock.create_order.reset_mock()
|
||||||
|
|
||||||
|
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||||
|
|
||||||
|
assert 'id' in order
|
||||||
|
assert 'info' in order
|
||||||
|
assert 'type' in order
|
||||||
|
|
||||||
|
assert order['type'] == order_type
|
||||||
|
assert order['price'] == 220
|
||||||
|
assert order['amount'] == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_stoploss_adjust_kucoin(mocker, default_conf):
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, id='kucoin')
|
||||||
|
order = {
|
||||||
|
'type': 'limit',
|
||||||
|
'price': 1500,
|
||||||
|
'stopPrice': 1500,
|
||||||
|
'info': {'stopPrice': 1500, 'stop': "limit"},
|
||||||
|
}
|
||||||
|
assert exchange.stoploss_adjust(1501, order)
|
||||||
|
assert not exchange.stoploss_adjust(1499, order)
|
||||||
|
# Test with invalid order case
|
||||||
|
order['info']['stop'] = None
|
||||||
|
assert not exchange.stoploss_adjust(1501, order)
|
@ -314,16 +314,15 @@ def test_backtesting_init_no_timeframe(mocker, default_conf, caplog) -> None:
|
|||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
del default_conf['timeframe']
|
del default_conf['timeframe']
|
||||||
default_conf['strategy_list'] = ['StrategyTestV2',
|
default_conf['strategy_list'] = ['StrategyTestV2',
|
||||||
'SampleStrategy']
|
'HyperoptableStrategy']
|
||||||
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.5))
|
mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.5))
|
||||||
with pytest.raises(OperationalException):
|
with pytest.raises(OperationalException,
|
||||||
|
match=r"Timeframe needs to be set in either configuration"):
|
||||||
Backtesting(default_conf)
|
Backtesting(default_conf)
|
||||||
log_has("Ticker-interval needs to be set in either configuration "
|
|
||||||
"or as cli argument `--ticker-interval 5m`", caplog)
|
|
||||||
|
|
||||||
|
|
||||||
def test_data_with_fee(default_conf, mocker, testdatadir) -> None:
|
def test_data_with_fee(default_conf, mocker) -> None:
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
default_conf['fee'] = 0.1234
|
default_conf['fee'] = 0.1234
|
||||||
|
|
||||||
|
@ -6,8 +6,7 @@ from unittest.mock import MagicMock
|
|||||||
from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_edge
|
from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_edge
|
||||||
from freqtrade.enums import RunMode
|
from freqtrade.enums import RunMode
|
||||||
from freqtrade.optimize.edge_cli import EdgeCli
|
from freqtrade.optimize.edge_cli import EdgeCli
|
||||||
from tests.conftest import (get_args, log_has, log_has_re, patch_exchange,
|
from tests.conftest import get_args, log_has, patch_exchange, patched_configuration_load_config_file
|
||||||
patched_configuration_load_config_file)
|
|
||||||
|
|
||||||
|
|
||||||
def test_setup_optimize_configuration_without_arguments(mocker, default_conf, caplog) -> None:
|
def test_setup_optimize_configuration_without_arguments(mocker, default_conf, caplog) -> None:
|
||||||
@ -30,7 +29,6 @@ def test_setup_optimize_configuration_without_arguments(mocker, default_conf, ca
|
|||||||
assert 'datadir' in config
|
assert 'datadir' in config
|
||||||
assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog)
|
assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog)
|
||||||
assert 'timeframe' in config
|
assert 'timeframe' in config
|
||||||
assert not log_has_re('Parameter -i/--ticker-interval detected .*', caplog)
|
|
||||||
|
|
||||||
assert 'timerange' not in config
|
assert 'timerange' not in config
|
||||||
assert 'stoploss_range' not in config
|
assert 'stoploss_range' not in config
|
||||||
|
@ -63,7 +63,6 @@ def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, ca
|
|||||||
assert 'datadir' in config
|
assert 'datadir' in config
|
||||||
assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog)
|
assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog)
|
||||||
assert 'timeframe' in config
|
assert 'timeframe' in config
|
||||||
assert not log_has_re('Parameter -i/--ticker-interval detected .*', caplog)
|
|
||||||
|
|
||||||
assert 'position_stacking' not in config
|
assert 'position_stacking' not in config
|
||||||
assert not log_has('Parameter --enable-position-stacking detected ...', caplog)
|
assert not log_has('Parameter --enable-position-stacking detected ...', caplog)
|
||||||
|
@ -782,6 +782,19 @@ def test_gen_pair_whitelist_not_supported(mocker, default_conf, tickers) -> None
|
|||||||
get_patched_freqtradebot(mocker, default_conf)
|
get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
|
||||||
|
|
||||||
|
def test_pair_whitelist_not_supported_Spread(mocker, default_conf, tickers) -> None:
|
||||||
|
default_conf['pairlists'] = [{'method': 'StaticPairList'}, {'method': 'SpreadFilter'}]
|
||||||
|
|
||||||
|
mocker.patch.multiple('freqtrade.exchange.Exchange',
|
||||||
|
get_tickers=tickers,
|
||||||
|
exchange_has=MagicMock(return_value=False),
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(OperationalException,
|
||||||
|
match=r'Exchange does not support fetchTickers, .*'):
|
||||||
|
get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("pairlist", AVAILABLE_PAIRLISTS)
|
@pytest.mark.parametrize("pairlist", AVAILABLE_PAIRLISTS)
|
||||||
def test_pairlist_class(mocker, whitelist_conf, markets, pairlist):
|
def test_pairlist_class(mocker, whitelist_conf, markets, pairlist):
|
||||||
whitelist_conf['pairlists'][0]['method'] = pairlist
|
whitelist_conf['pairlists'][0]['method'] = pairlist
|
||||||
|
@ -79,7 +79,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
|||||||
'close_rate': None,
|
'close_rate': None,
|
||||||
'current_rate': 1.099e-05,
|
'current_rate': 1.099e-05,
|
||||||
'amount': 91.07468123,
|
'amount': 91.07468123,
|
||||||
'amount_requested': 91.07468123,
|
'amount_requested': 91.07468124,
|
||||||
'stake_amount': 0.001,
|
'stake_amount': 0.001,
|
||||||
'trade_duration': None,
|
'trade_duration': None,
|
||||||
'trade_duration_s': None,
|
'trade_duration_s': None,
|
||||||
@ -109,14 +109,13 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
|||||||
'stoploss_entry_dist_ratio': -0.10448878,
|
'stoploss_entry_dist_ratio': -0.10448878,
|
||||||
'open_order': None,
|
'open_order': None,
|
||||||
'exchange': 'binance',
|
'exchange': 'binance',
|
||||||
'filled_entry_orders': [{
|
'orders': [{
|
||||||
'amount': 91.07468123, 'average': 1.098e-05, 'safe_price': 1.098e-05,
|
'amount': 91.07468123, 'average': 1.098e-05, 'safe_price': 1.098e-05,
|
||||||
'cost': 0.0009999999999054, 'filled': 91.07468123, 'ft_order_side': 'buy',
|
'cost': 0.0009999999999054, 'filled': 91.07468123, 'ft_order_side': 'buy',
|
||||||
'order_date': ANY, 'order_timestamp': ANY, 'order_filled_date': ANY,
|
'order_date': ANY, 'order_timestamp': ANY, 'order_filled_date': ANY,
|
||||||
'order_filled_timestamp': ANY, 'order_type': 'limit', 'price': 1.098e-05,
|
'order_filled_timestamp': ANY, 'order_type': 'limit', 'price': 1.098e-05,
|
||||||
'is_open': False, 'pair': 'ETH/BTC',
|
'is_open': False, 'pair': 'ETH/BTC', 'order_id': ANY,
|
||||||
'remaining': ANY, 'status': ANY}],
|
'remaining': ANY, 'status': ANY}],
|
||||||
'filled_exit_orders': []
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_rate',
|
mocker.patch('freqtrade.exchange.Exchange.get_rate',
|
||||||
@ -154,7 +153,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
|||||||
'close_rate': None,
|
'close_rate': None,
|
||||||
'current_rate': ANY,
|
'current_rate': ANY,
|
||||||
'amount': 91.07468123,
|
'amount': 91.07468123,
|
||||||
'amount_requested': 91.07468123,
|
'amount_requested': 91.07468124,
|
||||||
'trade_duration': ANY,
|
'trade_duration': ANY,
|
||||||
'trade_duration_s': ANY,
|
'trade_duration_s': ANY,
|
||||||
'stake_amount': 0.001,
|
'stake_amount': 0.001,
|
||||||
@ -184,14 +183,13 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
|||||||
'stoploss_entry_dist_ratio': -0.10448878,
|
'stoploss_entry_dist_ratio': -0.10448878,
|
||||||
'open_order': None,
|
'open_order': None,
|
||||||
'exchange': 'binance',
|
'exchange': 'binance',
|
||||||
'filled_entry_orders': [{
|
'orders': [{
|
||||||
'amount': 91.07468123, 'average': 1.098e-05, 'safe_price': 1.098e-05,
|
'amount': 91.07468123, 'average': 1.098e-05, 'safe_price': 1.098e-05,
|
||||||
'cost': 0.0009999999999054, 'filled': 91.07468123, 'ft_order_side': 'buy',
|
'cost': 0.0009999999999054, 'filled': 91.07468123, 'ft_order_side': 'buy',
|
||||||
'order_date': ANY, 'order_timestamp': ANY, 'order_filled_date': ANY,
|
'order_date': ANY, 'order_timestamp': ANY, 'order_filled_date': ANY,
|
||||||
'order_filled_timestamp': ANY, 'order_type': 'limit', 'price': 1.098e-05,
|
'order_filled_timestamp': ANY, 'order_type': 'limit', 'price': 1.098e-05,
|
||||||
'is_open': False, 'pair': 'ETH/BTC',
|
'is_open': False, 'pair': 'ETH/BTC', 'order_id': ANY,
|
||||||
'remaining': ANY, 'status': ANY}],
|
'remaining': ANY, 'status': ANY}],
|
||||||
'filled_exit_orders': []
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -607,8 +605,8 @@ def test_rpc_balance_handle(default_conf, mocker, tickers):
|
|||||||
rpc._fiat_converter = CryptoToFiatConverter()
|
rpc._fiat_converter = CryptoToFiatConverter()
|
||||||
|
|
||||||
result = rpc._rpc_balance(default_conf['stake_currency'], default_conf['fiat_display_currency'])
|
result = rpc._rpc_balance(default_conf['stake_currency'], default_conf['fiat_display_currency'])
|
||||||
assert prec_satoshi(result['total'], 12.309096315)
|
assert prec_satoshi(result['total'], 12.30909624)
|
||||||
assert prec_satoshi(result['value'], 184636.44472997)
|
assert prec_satoshi(result['value'], 184636.443606915)
|
||||||
assert tickers.call_count == 1
|
assert tickers.call_count == 1
|
||||||
assert tickers.call_args_list[0][1]['cached'] is True
|
assert tickers.call_args_list[0][1]['cached'] is True
|
||||||
assert 'USD' == result['symbol']
|
assert 'USD' == result['symbol']
|
||||||
@ -626,17 +624,16 @@ def test_rpc_balance_handle(default_conf, mocker, tickers):
|
|||||||
'est_stake': 0.30794,
|
'est_stake': 0.30794,
|
||||||
'used': 4.0,
|
'used': 4.0,
|
||||||
'stake': 'BTC',
|
'stake': 'BTC',
|
||||||
|
|
||||||
},
|
},
|
||||||
{'free': 5.0,
|
{'free': 5.0,
|
||||||
'balance': 10.0,
|
'balance': 10.0,
|
||||||
'currency': 'USDT',
|
'currency': 'USDT',
|
||||||
'est_stake': 0.0011563153318162476,
|
'est_stake': 0.0011562404610161968,
|
||||||
'used': 5.0,
|
'used': 5.0,
|
||||||
'stake': 'BTC',
|
'stake': 'BTC',
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
assert result['total'] == 12.309096315331816
|
assert result['total'] == 12.309096240461017
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_start(mocker, default_conf) -> None:
|
def test_rpc_start(mocker, default_conf) -> None:
|
||||||
@ -1150,6 +1147,7 @@ def test_rpcforcebuy(mocker, default_conf, ticker, fee, limit_buy_order_open) ->
|
|||||||
pair = 'LTC/BTC'
|
pair = 'LTC/BTC'
|
||||||
trade = rpc._rpc_forcebuy(pair, 0.0001, order_type='limit', stake_amount=0.05)
|
trade = rpc._rpc_forcebuy(pair, 0.0001, order_type='limit', stake_amount=0.05)
|
||||||
assert trade.stake_amount == 0.05
|
assert trade.stake_amount == 0.05
|
||||||
|
assert trade.buy_tag == 'forceentry'
|
||||||
|
|
||||||
# Test not buying
|
# Test not buying
|
||||||
pair = 'XRP/BTC'
|
pair = 'XRP/BTC'
|
||||||
|
@ -902,6 +902,8 @@ def test_api_status(botclient, mocker, ticker, fee, markets):
|
|||||||
'buy_tag': None,
|
'buy_tag': None,
|
||||||
'timeframe': 5,
|
'timeframe': 5,
|
||||||
'exchange': 'binance',
|
'exchange': 'binance',
|
||||||
|
'orders': [ANY],
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_rate',
|
mocker.patch('freqtrade.exchange.Exchange.get_rate',
|
||||||
@ -1089,6 +1091,7 @@ def test_api_forcebuy(botclient, mocker, fee):
|
|||||||
'buy_tag': None,
|
'buy_tag': None,
|
||||||
'timeframe': 5,
|
'timeframe': 5,
|
||||||
'exchange': 'binance',
|
'exchange': 'binance',
|
||||||
|
'orders': [],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -203,7 +203,7 @@ def test_telegram_status(default_conf, update, mocker) -> None:
|
|||||||
'stop_loss_ratio': -0.0001,
|
'stop_loss_ratio': -0.0001,
|
||||||
'open_order': '(limit buy rem=0.00000000)',
|
'open_order': '(limit buy rem=0.00000000)',
|
||||||
'is_open': True,
|
'is_open': True,
|
||||||
'filled_entry_orders': []
|
'orders': []
|
||||||
}]),
|
}]),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
|
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
|
||||||
|
|
||||||
import talib.abstract as ta
|
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
from strategy_test_v2 import StrategyTestV2
|
||||||
|
|
||||||
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
||||||
from freqtrade.strategy import (BooleanParameter, DecimalParameter, IntParameter, IStrategy,
|
from freqtrade.strategy import BooleanParameter, DecimalParameter, IntParameter, RealParameter
|
||||||
RealParameter)
|
|
||||||
|
|
||||||
|
|
||||||
class HyperoptableStrategy(IStrategy):
|
class HyperoptableStrategy(StrategyTestV2):
|
||||||
"""
|
"""
|
||||||
Default Strategy provided by freqtrade bot.
|
Default Strategy provided by freqtrade bot.
|
||||||
Please do not modify this strategy, it's intended for internal use only.
|
Please do not modify this strategy, it's intended for internal use only.
|
||||||
@ -16,38 +15,6 @@ class HyperoptableStrategy(IStrategy):
|
|||||||
or strategy repository https://github.com/freqtrade/freqtrade-strategies
|
or strategy repository https://github.com/freqtrade/freqtrade-strategies
|
||||||
for samples and inspiration.
|
for samples and inspiration.
|
||||||
"""
|
"""
|
||||||
INTERFACE_VERSION = 2
|
|
||||||
|
|
||||||
# Minimal ROI designed for the strategy
|
|
||||||
minimal_roi = {
|
|
||||||
"40": 0.0,
|
|
||||||
"30": 0.01,
|
|
||||||
"20": 0.02,
|
|
||||||
"0": 0.04
|
|
||||||
}
|
|
||||||
|
|
||||||
# Optimal stoploss designed for the strategy
|
|
||||||
stoploss = -0.10
|
|
||||||
|
|
||||||
# Optimal ticker interval for the strategy
|
|
||||||
timeframe = '5m'
|
|
||||||
|
|
||||||
# Optional order type mapping
|
|
||||||
order_types = {
|
|
||||||
'buy': 'limit',
|
|
||||||
'sell': 'limit',
|
|
||||||
'stoploss': 'limit',
|
|
||||||
'stoploss_on_exchange': False
|
|
||||||
}
|
|
||||||
|
|
||||||
# Number of candles the strategy requires before producing valid signals
|
|
||||||
startup_candle_count: int = 20
|
|
||||||
|
|
||||||
# Optional time in force for orders
|
|
||||||
order_time_in_force = {
|
|
||||||
'buy': 'gtc',
|
|
||||||
'sell': 'gtc',
|
|
||||||
}
|
|
||||||
|
|
||||||
buy_params = {
|
buy_params = {
|
||||||
'buy_rsi': 35,
|
'buy_rsi': 35,
|
||||||
@ -91,55 +58,6 @@ class HyperoptableStrategy(IStrategy):
|
|||||||
"""
|
"""
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
|
||||||
"""
|
|
||||||
Adds several different TA indicators to the given DataFrame
|
|
||||||
|
|
||||||
Performance Note: For the best performance be frugal on the number of indicators
|
|
||||||
you are using. Let uncomment only the indicator you are using in your strategies
|
|
||||||
or your hyperopt configuration, otherwise you will waste your memory and CPU usage.
|
|
||||||
:param dataframe: Dataframe with data from the exchange
|
|
||||||
:param metadata: Additional information, like the currently traded pair
|
|
||||||
:return: a Dataframe with all mandatory indicators for the strategies
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Momentum Indicator
|
|
||||||
# ------------------------------------
|
|
||||||
|
|
||||||
# ADX
|
|
||||||
dataframe['adx'] = ta.ADX(dataframe)
|
|
||||||
|
|
||||||
# MACD
|
|
||||||
macd = ta.MACD(dataframe)
|
|
||||||
dataframe['macd'] = macd['macd']
|
|
||||||
dataframe['macdsignal'] = macd['macdsignal']
|
|
||||||
dataframe['macdhist'] = macd['macdhist']
|
|
||||||
|
|
||||||
# Minus Directional Indicator / Movement
|
|
||||||
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
|
|
||||||
|
|
||||||
# Plus Directional Indicator / Movement
|
|
||||||
dataframe['plus_di'] = ta.PLUS_DI(dataframe)
|
|
||||||
|
|
||||||
# RSI
|
|
||||||
dataframe['rsi'] = ta.RSI(dataframe)
|
|
||||||
|
|
||||||
# Stoch fast
|
|
||||||
stoch_fast = ta.STOCHF(dataframe)
|
|
||||||
dataframe['fastd'] = stoch_fast['fastd']
|
|
||||||
dataframe['fastk'] = stoch_fast['fastk']
|
|
||||||
|
|
||||||
# Bollinger bands
|
|
||||||
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
|
|
||||||
dataframe['bb_lowerband'] = bollinger['lower']
|
|
||||||
dataframe['bb_middleband'] = bollinger['mid']
|
|
||||||
dataframe['bb_upperband'] = bollinger['upper']
|
|
||||||
|
|
||||||
# EMA - Exponential Moving Average
|
|
||||||
dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10)
|
|
||||||
|
|
||||||
return dataframe
|
|
||||||
|
|
||||||
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Based on TA indicators, populates the buy signal for the given dataframe
|
Based on TA indicators, populates the buy signal for the given dataframe
|
||||||
|
@ -31,9 +31,7 @@ class TestStrategyLegacyV1(IStrategy):
|
|||||||
# This attribute will be overridden if the config file contains "stoploss"
|
# This attribute will be overridden if the config file contains "stoploss"
|
||||||
stoploss = -0.10
|
stoploss = -0.10
|
||||||
|
|
||||||
# Optimal timeframe for the strategy
|
timeframe = '5m'
|
||||||
# Keep the legacy value here to test compatibility
|
|
||||||
ticker_interval = '5m'
|
|
||||||
|
|
||||||
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
|
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
|
@ -7,7 +7,7 @@ from pandas import DataFrame
|
|||||||
|
|
||||||
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
from freqtrade.strategy.interface import IStrategy
|
from freqtrade.strategy import IStrategy
|
||||||
|
|
||||||
|
|
||||||
class StrategyTestV2(IStrategy):
|
class StrategyTestV2(IStrategy):
|
||||||
|
@ -111,7 +111,6 @@ def test_strategy(result, default_conf):
|
|||||||
assert default_conf['stoploss'] == -0.10
|
assert default_conf['stoploss'] == -0.10
|
||||||
|
|
||||||
assert strategy.timeframe == '5m'
|
assert strategy.timeframe == '5m'
|
||||||
assert strategy.ticker_interval == '5m'
|
|
||||||
assert default_conf['timeframe'] == '5m'
|
assert default_conf['timeframe'] == '5m'
|
||||||
|
|
||||||
df_indicators = strategy.advise_indicators(result, metadata=metadata)
|
df_indicators = strategy.advise_indicators(result, metadata=metadata)
|
||||||
@ -376,7 +375,6 @@ def test_call_deprecated_function(result, monkeypatch, default_conf, caplog):
|
|||||||
assert strategy._sell_fun_len == 2
|
assert strategy._sell_fun_len == 2
|
||||||
assert strategy.INTERFACE_VERSION == 1
|
assert strategy.INTERFACE_VERSION == 1
|
||||||
assert strategy.timeframe == '5m'
|
assert strategy.timeframe == '5m'
|
||||||
assert strategy.ticker_interval == '5m'
|
|
||||||
|
|
||||||
indicator_df = strategy.advise_indicators(result, metadata=metadata)
|
indicator_df = strategy.advise_indicators(result, metadata=metadata)
|
||||||
assert isinstance(indicator_df, DataFrame)
|
assert isinstance(indicator_df, DataFrame)
|
||||||
@ -390,9 +388,6 @@ def test_call_deprecated_function(result, monkeypatch, default_conf, caplog):
|
|||||||
assert isinstance(selldf, DataFrame)
|
assert isinstance(selldf, DataFrame)
|
||||||
assert 'sell' in selldf
|
assert 'sell' in selldf
|
||||||
|
|
||||||
assert log_has("DEPRECATED: Please migrate to using 'timeframe' instead of 'ticker_interval'.",
|
|
||||||
caplog)
|
|
||||||
|
|
||||||
|
|
||||||
def test_strategy_interface_versioning(result, monkeypatch, default_conf):
|
def test_strategy_interface_versioning(result, monkeypatch, default_conf):
|
||||||
default_conf.update({'strategy': 'StrategyTestV2'})
|
default_conf.update({'strategy': 'StrategyTestV2'})
|
||||||
|
@ -111,17 +111,17 @@ def test_parse_args_strategy_path_invalid() -> None:
|
|||||||
|
|
||||||
def test_parse_args_backtesting_invalid() -> None:
|
def test_parse_args_backtesting_invalid() -> None:
|
||||||
with pytest.raises(SystemExit, match=r'2'):
|
with pytest.raises(SystemExit, match=r'2'):
|
||||||
Arguments(['backtesting --ticker-interval']).get_parsed_arg()
|
Arguments(['backtesting --timeframe']).get_parsed_arg()
|
||||||
|
|
||||||
with pytest.raises(SystemExit, match=r'2'):
|
with pytest.raises(SystemExit, match=r'2'):
|
||||||
Arguments(['backtesting --ticker-interval', 'abc']).get_parsed_arg()
|
Arguments(['backtesting --timeframe', 'abc']).get_parsed_arg()
|
||||||
|
|
||||||
|
|
||||||
def test_parse_args_backtesting_custom() -> None:
|
def test_parse_args_backtesting_custom() -> None:
|
||||||
args = [
|
args = [
|
||||||
'backtesting',
|
'backtesting',
|
||||||
'-c', 'test_conf.json',
|
'-c', 'test_conf.json',
|
||||||
'--ticker-interval', '1m',
|
'--timeframe', '1m',
|
||||||
'--strategy-list',
|
'--strategy-list',
|
||||||
'StrategyTestV2',
|
'StrategyTestV2',
|
||||||
'SampleStrategy'
|
'SampleStrategy'
|
||||||
|
@ -443,7 +443,7 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
|
|||||||
'--strategy', 'StrategyTestV2',
|
'--strategy', 'StrategyTestV2',
|
||||||
'--datadir', '/foo/bar',
|
'--datadir', '/foo/bar',
|
||||||
'--userdir', "/tmp/freqtrade",
|
'--userdir', "/tmp/freqtrade",
|
||||||
'--ticker-interval', '1m',
|
'--timeframe', '1m',
|
||||||
'--enable-position-stacking',
|
'--enable-position-stacking',
|
||||||
'--disable-max-market-positions',
|
'--disable-max-market-positions',
|
||||||
'--timerange', ':100',
|
'--timerange', ':100',
|
||||||
@ -494,7 +494,7 @@ def test_setup_configuration_with_stratlist(mocker, default_conf, caplog) -> Non
|
|||||||
arglist = [
|
arglist = [
|
||||||
'backtesting',
|
'backtesting',
|
||||||
'--config', 'config.json',
|
'--config', 'config.json',
|
||||||
'--ticker-interval', '1m',
|
'--timeframe', '1m',
|
||||||
'--export', 'trades',
|
'--export', 'trades',
|
||||||
'--strategy-list',
|
'--strategy-list',
|
||||||
'StrategyTestV2',
|
'StrategyTestV2',
|
||||||
@ -1320,22 +1320,14 @@ def test_process_removed_setting(mocker, default_conf, caplog):
|
|||||||
def test_process_deprecated_ticker_interval(default_conf, caplog):
|
def test_process_deprecated_ticker_interval(default_conf, caplog):
|
||||||
message = "DEPRECATED: Please use 'timeframe' instead of 'ticker_interval."
|
message = "DEPRECATED: Please use 'timeframe' instead of 'ticker_interval."
|
||||||
config = deepcopy(default_conf)
|
config = deepcopy(default_conf)
|
||||||
|
|
||||||
process_temporary_deprecated_settings(config)
|
process_temporary_deprecated_settings(config)
|
||||||
assert not log_has(message, caplog)
|
assert not log_has(message, caplog)
|
||||||
|
|
||||||
del config['timeframe']
|
del config['timeframe']
|
||||||
config['ticker_interval'] = '15m'
|
config['ticker_interval'] = '15m'
|
||||||
process_temporary_deprecated_settings(config)
|
|
||||||
assert log_has(message, caplog)
|
|
||||||
assert config['ticker_interval'] == '15m'
|
|
||||||
|
|
||||||
config = deepcopy(default_conf)
|
|
||||||
# Have both timeframe and ticker interval in config
|
|
||||||
# Can also happen when using ticker_interval in configuration, and --timeframe as cli argument
|
|
||||||
config['timeframe'] = '5m'
|
|
||||||
config['ticker_interval'] = '4h'
|
|
||||||
with pytest.raises(OperationalException,
|
with pytest.raises(OperationalException,
|
||||||
match=r"Both 'timeframe' and 'ticker_interval' detected."):
|
match=r"DEPRECATED: 'ticker_interval' detected. Please use.*"):
|
||||||
process_temporary_deprecated_settings(config)
|
process_temporary_deprecated_settings(config)
|
||||||
|
|
||||||
|
|
||||||
|
@ -727,7 +727,7 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_buy_order_usdt,
|
|||||||
call_args = buy_mm.call_args_list[0][1]
|
call_args = buy_mm.call_args_list[0][1]
|
||||||
assert call_args['pair'] == pair
|
assert call_args['pair'] == pair
|
||||||
assert call_args['rate'] == bid
|
assert call_args['rate'] == bid
|
||||||
assert call_args['amount'] == round(stake_amount / bid, 8)
|
assert call_args['amount'] == stake_amount / bid
|
||||||
buy_rate_mock.reset_mock()
|
buy_rate_mock.reset_mock()
|
||||||
|
|
||||||
# Should create an open trade with an open order id
|
# Should create an open trade with an open order id
|
||||||
@ -748,7 +748,7 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_buy_order_usdt,
|
|||||||
call_args = buy_mm.call_args_list[1][1]
|
call_args = buy_mm.call_args_list[1][1]
|
||||||
assert call_args['pair'] == pair
|
assert call_args['pair'] == pair
|
||||||
assert call_args['rate'] == fix_price
|
assert call_args['rate'] == fix_price
|
||||||
assert call_args['amount'] == round(stake_amount / fix_price, 8)
|
assert call_args['amount'] == stake_amount / fix_price
|
||||||
|
|
||||||
# In case of closed order
|
# In case of closed order
|
||||||
limit_buy_order_usdt['status'] = 'closed'
|
limit_buy_order_usdt['status'] = 'closed'
|
||||||
@ -926,12 +926,10 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog,
|
|||||||
}),
|
}),
|
||||||
create_order=MagicMock(side_effect=[
|
create_order=MagicMock(side_effect=[
|
||||||
{'id': limit_buy_order_usdt['id']},
|
{'id': limit_buy_order_usdt['id']},
|
||||||
{'id': limit_sell_order_usdt['id']},
|
limit_sell_order_usdt,
|
||||||
|
# {'id': limit_sell_order_usdt['id']},
|
||||||
]),
|
]),
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
)
|
|
||||||
mocker.patch.multiple(
|
|
||||||
'freqtrade.exchange.Binance',
|
|
||||||
stoploss=stoploss
|
stoploss=stoploss
|
||||||
)
|
)
|
||||||
freqtrade = FreqtradeBot(default_conf_usdt)
|
freqtrade = FreqtradeBot(default_conf_usdt)
|
||||||
@ -956,7 +954,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog,
|
|||||||
trade.stoploss_order_id = 100
|
trade.stoploss_order_id = 100
|
||||||
|
|
||||||
hanging_stoploss_order = MagicMock(return_value={'status': 'open'})
|
hanging_stoploss_order = MagicMock(return_value={'status': 'open'})
|
||||||
mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order', hanging_stoploss_order)
|
mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', hanging_stoploss_order)
|
||||||
|
|
||||||
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||||
assert trade.stoploss_order_id == 100
|
assert trade.stoploss_order_id == 100
|
||||||
@ -969,7 +967,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog,
|
|||||||
trade.stoploss_order_id = 100
|
trade.stoploss_order_id = 100
|
||||||
|
|
||||||
canceled_stoploss_order = MagicMock(return_value={'status': 'canceled'})
|
canceled_stoploss_order = MagicMock(return_value={'status': 'canceled'})
|
||||||
mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order', canceled_stoploss_order)
|
mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', canceled_stoploss_order)
|
||||||
stoploss.reset_mock()
|
stoploss.reset_mock()
|
||||||
|
|
||||||
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||||
@ -1001,7 +999,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog,
|
|||||||
'average': 2,
|
'average': 2,
|
||||||
'amount': limit_buy_order_usdt['amount'],
|
'amount': limit_buy_order_usdt['amount'],
|
||||||
})
|
})
|
||||||
mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order', stoploss_order_hit)
|
mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_order_hit)
|
||||||
assert freqtrade.handle_stoploss_on_exchange(trade) is True
|
assert freqtrade.handle_stoploss_on_exchange(trade) is True
|
||||||
assert log_has_re(r'STOP_LOSS_LIMIT is hit for Trade\(id=1, .*\)\.', caplog)
|
assert log_has_re(r'STOP_LOSS_LIMIT is hit for Trade\(id=1, .*\)\.', caplog)
|
||||||
assert trade.stoploss_order_id is None
|
assert trade.stoploss_order_id is None
|
||||||
@ -1009,7 +1007,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog,
|
|||||||
caplog.clear()
|
caplog.clear()
|
||||||
|
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
'freqtrade.exchange.Binance.stoploss',
|
'freqtrade.exchange.Exchange.stoploss',
|
||||||
side_effect=ExchangeError()
|
side_effect=ExchangeError()
|
||||||
)
|
)
|
||||||
trade.is_open = True
|
trade.is_open = True
|
||||||
@ -1021,9 +1019,9 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog,
|
|||||||
# It should try to add stoploss order
|
# It should try to add stoploss order
|
||||||
trade.stoploss_order_id = 100
|
trade.stoploss_order_id = 100
|
||||||
stoploss.reset_mock()
|
stoploss.reset_mock()
|
||||||
mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order',
|
mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order',
|
||||||
side_effect=InvalidOrderException())
|
side_effect=InvalidOrderException())
|
||||||
mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss)
|
mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss)
|
||||||
freqtrade.handle_stoploss_on_exchange(trade)
|
freqtrade.handle_stoploss_on_exchange(trade)
|
||||||
assert stoploss.call_count == 1
|
assert stoploss.call_count == 1
|
||||||
|
|
||||||
@ -1033,10 +1031,37 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog,
|
|||||||
trade.is_open = False
|
trade.is_open = False
|
||||||
stoploss.reset_mock()
|
stoploss.reset_mock()
|
||||||
mocker.patch('freqtrade.exchange.Exchange.fetch_order')
|
mocker.patch('freqtrade.exchange.Exchange.fetch_order')
|
||||||
mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss)
|
mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss)
|
||||||
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||||
assert stoploss.call_count == 0
|
assert stoploss.call_count == 0
|
||||||
|
|
||||||
|
# Seventh case: emergency exit triggered
|
||||||
|
# Trailing stop should not act anymore
|
||||||
|
stoploss_order_cancelled = MagicMock(side_effect=[{
|
||||||
|
'id': "100",
|
||||||
|
'status': 'canceled',
|
||||||
|
'type': 'stop_loss_limit',
|
||||||
|
'price': 3,
|
||||||
|
'average': 2,
|
||||||
|
'amount': limit_buy_order_usdt['amount'],
|
||||||
|
'info': {'stopPrice': 22},
|
||||||
|
}])
|
||||||
|
trade.stoploss_order_id = 100
|
||||||
|
trade.is_open = True
|
||||||
|
trade.stoploss_last_update = arrow.utcnow().shift(hours=-1).datetime
|
||||||
|
trade.stop_loss = 24
|
||||||
|
freqtrade.config['trailing_stop'] = True
|
||||||
|
stoploss = MagicMock(side_effect=InvalidOrderException())
|
||||||
|
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order_with_result',
|
||||||
|
side_effect=InvalidOrderException())
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_order_cancelled)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss)
|
||||||
|
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||||
|
assert trade.stoploss_order_id is None
|
||||||
|
assert trade.is_open is False
|
||||||
|
assert trade.sell_reason == str(SellType.EMERGENCY_SELL)
|
||||||
|
|
||||||
|
|
||||||
def test_handle_sle_cancel_cant_recreate(mocker, default_conf_usdt, fee, caplog,
|
def test_handle_sle_cancel_cant_recreate(mocker, default_conf_usdt, fee, caplog,
|
||||||
limit_buy_order_usdt, limit_sell_order_usdt) -> None:
|
limit_buy_order_usdt, limit_sell_order_usdt) -> None:
|
||||||
@ -1266,7 +1291,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf_usdt, fee,
|
|||||||
|
|
||||||
cancel_order_mock.assert_called_once_with(100, 'ETH/USDT')
|
cancel_order_mock.assert_called_once_with(100, 'ETH/USDT')
|
||||||
stoploss_order_mock.assert_called_once_with(
|
stoploss_order_mock.assert_called_once_with(
|
||||||
amount=27.39726027,
|
amount=pytest.approx(27.39726027),
|
||||||
pair='ETH/USDT',
|
pair='ETH/USDT',
|
||||||
order_types=freqtrade.strategy.order_types,
|
order_types=freqtrade.strategy.order_types,
|
||||||
stop_price=4.4 * 0.95
|
stop_price=4.4 * 0.95
|
||||||
@ -1360,6 +1385,32 @@ def test_handle_stoploss_on_exchange_trailing_error(
|
|||||||
assert log_has_re(r"Could not create trailing stoploss order for pair ETH/USDT\..*", caplog)
|
assert log_has_re(r"Could not create trailing stoploss order for pair ETH/USDT\..*", caplog)
|
||||||
|
|
||||||
|
|
||||||
|
def test_stoploss_on_exchange_price_rounding(
|
||||||
|
mocker, default_conf_usdt, fee, open_trade_usdt) -> None:
|
||||||
|
patch_RPCManager(mocker)
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
get_fee=fee,
|
||||||
|
)
|
||||||
|
price_mock = MagicMock(side_effect=lambda p, s: int(s))
|
||||||
|
stoploss_mock = MagicMock(return_value={'id': '13434334'})
|
||||||
|
adjust_mock = MagicMock(return_value=False)
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Binance',
|
||||||
|
stoploss=stoploss_mock,
|
||||||
|
stoploss_adjust=adjust_mock,
|
||||||
|
price_to_precision=price_mock,
|
||||||
|
)
|
||||||
|
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||||
|
open_trade_usdt.stoploss_order_id = '13434334'
|
||||||
|
open_trade_usdt.stop_loss = 222.55
|
||||||
|
|
||||||
|
freqtrade.handle_trailing_stoploss_on_exchange(open_trade_usdt, {})
|
||||||
|
assert price_mock.call_count == 1
|
||||||
|
assert adjust_mock.call_count == 1
|
||||||
|
assert adjust_mock.call_args_list[0][0][0] == 222
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("init_persistence")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
def test_handle_stoploss_on_exchange_custom_stop(
|
def test_handle_stoploss_on_exchange_custom_stop(
|
||||||
mocker, default_conf_usdt, fee, limit_buy_order_usdt, limit_sell_order_usdt) -> None:
|
mocker, default_conf_usdt, fee, limit_buy_order_usdt, limit_sell_order_usdt) -> None:
|
||||||
@ -1458,7 +1509,7 @@ def test_handle_stoploss_on_exchange_custom_stop(
|
|||||||
|
|
||||||
cancel_order_mock.assert_called_once_with(100, 'ETH/USDT')
|
cancel_order_mock.assert_called_once_with(100, 'ETH/USDT')
|
||||||
stoploss_order_mock.assert_called_once_with(
|
stoploss_order_mock.assert_called_once_with(
|
||||||
amount=31.57894736,
|
amount=pytest.approx(31.57894736),
|
||||||
pair='ETH/USDT',
|
pair='ETH/USDT',
|
||||||
order_types=freqtrade.strategy.order_types,
|
order_types=freqtrade.strategy.order_types,
|
||||||
stop_price=4.4 * 0.96
|
stop_price=4.4 * 0.96
|
||||||
@ -1583,7 +1634,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee,
|
|||||||
assert trade.stop_loss == 4.4 * 0.99
|
assert trade.stop_loss == 4.4 * 0.99
|
||||||
cancel_order_mock.assert_called_once_with(100, 'NEO/BTC')
|
cancel_order_mock.assert_called_once_with(100, 'NEO/BTC')
|
||||||
stoploss_order_mock.assert_called_once_with(
|
stoploss_order_mock.assert_called_once_with(
|
||||||
amount=11.41438356,
|
amount=pytest.approx(11.41438356),
|
||||||
pair='NEO/BTC',
|
pair='NEO/BTC',
|
||||||
order_types=freqtrade.strategy.order_types,
|
order_types=freqtrade.strategy.order_types,
|
||||||
stop_price=4.4 * 0.99
|
stop_price=4.4 * 0.99
|
||||||
@ -2554,9 +2605,12 @@ def test_handle_cancel_exit_limit(mocker, default_conf_usdt, fee) -> None:
|
|||||||
exchange='binance',
|
exchange='binance',
|
||||||
open_rate=0.245441,
|
open_rate=0.245441,
|
||||||
open_order_id="123456",
|
open_order_id="123456",
|
||||||
open_date=arrow.utcnow().datetime,
|
open_date=arrow.utcnow().shift(days=-2).datetime,
|
||||||
fee_open=fee.return_value,
|
fee_open=fee.return_value,
|
||||||
fee_close=fee.return_value,
|
fee_close=fee.return_value,
|
||||||
|
close_rate=0.555,
|
||||||
|
close_date=arrow.utcnow().datetime,
|
||||||
|
sell_reason="sell_reason_whatever",
|
||||||
)
|
)
|
||||||
order = {'remaining': 1,
|
order = {'remaining': 1,
|
||||||
'amount': 1,
|
'amount': 1,
|
||||||
@ -2565,6 +2619,8 @@ def test_handle_cancel_exit_limit(mocker, default_conf_usdt, fee) -> None:
|
|||||||
assert freqtrade.handle_cancel_exit(trade, order, reason)
|
assert freqtrade.handle_cancel_exit(trade, order, reason)
|
||||||
assert cancel_order_mock.call_count == 1
|
assert cancel_order_mock.call_count == 1
|
||||||
assert send_msg_mock.call_count == 1
|
assert send_msg_mock.call_count == 1
|
||||||
|
assert trade.close_rate is None
|
||||||
|
assert trade.sell_reason is None
|
||||||
|
|
||||||
send_msg_mock.reset_mock()
|
send_msg_mock.reset_mock()
|
||||||
|
|
||||||
@ -3512,9 +3568,9 @@ def test_get_real_amount_quote(default_conf_usdt, trades_for_order, buy_order_fe
|
|||||||
open_order_id="123456"
|
open_order_id="123456"
|
||||||
)
|
)
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||||
|
order_obj = Order.parse_from_ccxt_object(buy_order_fee, 'LTC/ETH', 'buy')
|
||||||
# Amount is reduced by "fee"
|
# Amount is reduced by "fee"
|
||||||
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount - (amount * 0.001)
|
assert freqtrade.get_real_amount(trade, buy_order_fee, order_obj) == amount - (amount * 0.001)
|
||||||
assert log_has('Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, '
|
assert log_has('Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, '
|
||||||
'open_rate=0.24544100, open_since=closed) (from 8.0 to 7.992).',
|
'open_rate=0.24544100, open_since=closed) (from 8.0 to 7.992).',
|
||||||
caplog)
|
caplog)
|
||||||
@ -3538,8 +3594,9 @@ def test_get_real_amount_quote_dust(default_conf_usdt, trades_for_order, buy_ord
|
|||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||||
|
|
||||||
walletmock.reset_mock()
|
walletmock.reset_mock()
|
||||||
|
order_obj = Order.parse_from_ccxt_object(buy_order_fee, 'LTC/ETH', 'buy')
|
||||||
# Amount is kept as is
|
# Amount is kept as is
|
||||||
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount
|
assert freqtrade.get_real_amount(trade, buy_order_fee, order_obj) == amount
|
||||||
assert walletmock.call_count == 1
|
assert walletmock.call_count == 1
|
||||||
assert log_has_re(r'Fee amount for Trade.* was in base currency '
|
assert log_has_re(r'Fee amount for Trade.* was in base currency '
|
||||||
'- Eating Fee 0.008 into dust', caplog)
|
'- Eating Fee 0.008 into dust', caplog)
|
||||||
@ -3560,8 +3617,9 @@ def test_get_real_amount_no_trade(default_conf_usdt, buy_order_fee, caplog, mock
|
|||||||
)
|
)
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||||
|
|
||||||
|
order_obj = Order.parse_from_ccxt_object(buy_order_fee, 'LTC/ETH', 'buy')
|
||||||
# Amount is reduced by "fee"
|
# Amount is reduced by "fee"
|
||||||
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount
|
assert freqtrade.get_real_amount(trade, buy_order_fee, order_obj) == amount
|
||||||
assert log_has('Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, '
|
assert log_has('Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, '
|
||||||
'open_rate=0.24544100, open_since=closed) failed: myTrade-Dict empty found',
|
'open_rate=0.24544100, open_since=closed) failed: myTrade-Dict empty found',
|
||||||
caplog)
|
caplog)
|
||||||
@ -3612,7 +3670,8 @@ def test_get_real_amount(
|
|||||||
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', side_effect=ExchangeError)
|
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', side_effect=ExchangeError)
|
||||||
|
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
assert freqtrade.get_real_amount(trade, buy_order) == amount - fee_reduction_amount
|
order_obj = Order.parse_from_ccxt_object(buy_order_fee, 'LTC/ETH', 'buy')
|
||||||
|
assert freqtrade.get_real_amount(trade, buy_order, order_obj) == amount - fee_reduction_amount
|
||||||
|
|
||||||
if expected_log:
|
if expected_log:
|
||||||
assert log_has(expected_log, caplog)
|
assert log_has(expected_log, caplog)
|
||||||
@ -3659,7 +3718,8 @@ def test_get_real_amount_multi(
|
|||||||
|
|
||||||
# Amount is reduced by "fee"
|
# Amount is reduced by "fee"
|
||||||
expected_amount = amount - (amount * fee_reduction_amount)
|
expected_amount = amount - (amount * fee_reduction_amount)
|
||||||
assert freqtrade.get_real_amount(trade, buy_order_fee) == expected_amount
|
order_obj = Order.parse_from_ccxt_object(buy_order_fee, 'LTC/ETH', 'buy')
|
||||||
|
assert freqtrade.get_real_amount(trade, buy_order_fee, order_obj) == expected_amount
|
||||||
assert log_has(
|
assert log_has(
|
||||||
(
|
(
|
||||||
'Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, '
|
'Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, '
|
||||||
@ -3694,8 +3754,9 @@ def test_get_real_amount_invalid_order(default_conf_usdt, trades_for_order, buy_
|
|||||||
)
|
)
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||||
|
|
||||||
|
order_obj = Order.parse_from_ccxt_object(buy_order_fee, 'LTC/ETH', 'buy')
|
||||||
# Amount does not change
|
# Amount does not change
|
||||||
assert freqtrade.get_real_amount(trade, limit_buy_order_usdt) == amount
|
assert freqtrade.get_real_amount(trade, limit_buy_order_usdt, order_obj) == amount
|
||||||
|
|
||||||
|
|
||||||
def test_get_real_amount_fees_order(default_conf_usdt, market_buy_order_usdt_doublefee,
|
def test_get_real_amount_fees_order(default_conf_usdt, market_buy_order_usdt_doublefee,
|
||||||
@ -3717,7 +3778,8 @@ def test_get_real_amount_fees_order(default_conf_usdt, market_buy_order_usdt_dou
|
|||||||
|
|
||||||
# Amount does not change
|
# Amount does not change
|
||||||
assert trade.fee_open == 0.0025
|
assert trade.fee_open == 0.0025
|
||||||
assert freqtrade.get_real_amount(trade, market_buy_order_usdt_doublefee) == 30.0
|
order_obj = Order.parse_from_ccxt_object(market_buy_order_usdt_doublefee, 'LTC/ETH', 'buy')
|
||||||
|
assert freqtrade.get_real_amount(trade, market_buy_order_usdt_doublefee, order_obj) == 30.0
|
||||||
assert tfo_mock.call_count == 0
|
assert tfo_mock.call_count == 0
|
||||||
# Fetch fees from trades dict if available to get "proper" values
|
# Fetch fees from trades dict if available to get "proper" values
|
||||||
assert round(trade.fee_open, 4) == 0.001
|
assert round(trade.fee_open, 4) == 0.001
|
||||||
@ -3741,9 +3803,10 @@ def test_get_real_amount_wrong_amount(default_conf_usdt, trades_for_order, buy_o
|
|||||||
)
|
)
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||||
|
|
||||||
|
order_obj = Order.parse_from_ccxt_object(buy_order_fee, 'LTC/ETH', 'buy')
|
||||||
# Amount does not change
|
# Amount does not change
|
||||||
with pytest.raises(DependencyException, match=r"Half bought\? Amounts don't match"):
|
with pytest.raises(DependencyException, match=r"Half bought\? Amounts don't match"):
|
||||||
freqtrade.get_real_amount(trade, limit_buy_order_usdt)
|
freqtrade.get_real_amount(trade, limit_buy_order_usdt, order_obj)
|
||||||
|
|
||||||
|
|
||||||
def test_get_real_amount_wrong_amount_rounding(default_conf_usdt, trades_for_order, buy_order_fee,
|
def test_get_real_amount_wrong_amount_rounding(default_conf_usdt, trades_for_order, buy_order_fee,
|
||||||
@ -3765,9 +3828,10 @@ def test_get_real_amount_wrong_amount_rounding(default_conf_usdt, trades_for_ord
|
|||||||
)
|
)
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||||
|
|
||||||
|
order_obj = Order.parse_from_ccxt_object(buy_order_fee, 'LTC/ETH', 'buy')
|
||||||
# Amount changes by fee amount.
|
# Amount changes by fee amount.
|
||||||
assert isclose(
|
assert isclose(
|
||||||
freqtrade.get_real_amount(trade, limit_buy_order_usdt),
|
freqtrade.get_real_amount(trade, limit_buy_order_usdt, order_obj),
|
||||||
amount - (amount * 0.001),
|
amount - (amount * 0.001),
|
||||||
abs_tol=MATH_CLOSE_PREC,
|
abs_tol=MATH_CLOSE_PREC,
|
||||||
)
|
)
|
||||||
@ -3791,7 +3855,8 @@ def test_get_real_amount_open_trade(default_conf_usdt, fee, mocker):
|
|||||||
'side': 'buy',
|
'side': 'buy',
|
||||||
}
|
}
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||||
assert freqtrade.get_real_amount(trade, order) == amount
|
order_obj = Order.parse_from_ccxt_object(order, 'LTC/ETH', 'buy')
|
||||||
|
assert freqtrade.get_real_amount(trade, order, order_obj) == amount
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('amount,fee_abs,wallet,amount_exp', [
|
@pytest.mark.parametrize('amount,fee_abs,wallet,amount_exp', [
|
||||||
|
@ -26,7 +26,9 @@ def test_ttl_cache():
|
|||||||
assert 'a' in cache1h
|
assert 'a' in cache1h
|
||||||
|
|
||||||
t.move_to("2021-09-01 05:59:59 +00:00")
|
t.move_to("2021-09-01 05:59:59 +00:00")
|
||||||
|
assert 'a' not in cache
|
||||||
assert 'a' in cache1h
|
assert 'a' in cache1h
|
||||||
|
|
||||||
t.move_to("2021-09-01 06:00:00 +00:00")
|
t.move_to("2021-09-01 06:00:00 +00:00")
|
||||||
|
assert 'a' not in cache
|
||||||
assert 'a' not in cache1h
|
assert 'a' not in cache1h
|
||||||
|
@ -901,8 +901,7 @@ def test_to_json(default_conf, fee):
|
|||||||
'buy_tag': None,
|
'buy_tag': None,
|
||||||
'timeframe': None,
|
'timeframe': None,
|
||||||
'exchange': 'binance',
|
'exchange': 'binance',
|
||||||
'filled_entry_orders': [],
|
'orders': [],
|
||||||
'filled_exit_orders': []
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Simulate dry_run entries
|
# Simulate dry_run entries
|
||||||
@ -970,8 +969,7 @@ def test_to_json(default_conf, fee):
|
|||||||
'buy_tag': 'buys_signal_001',
|
'buy_tag': 'buys_signal_001',
|
||||||
'timeframe': None,
|
'timeframe': None,
|
||||||
'exchange': 'binance',
|
'exchange': 'binance',
|
||||||
'filled_entry_orders': [],
|
'orders': [],
|
||||||
'filled_exit_orders': []
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user