Merge branch 'develop' into timeframe
This commit is contained in:
commit
a3506f4d8e
@ -63,8 +63,8 @@ class SuperDuperHyperOptLoss(IHyperOptLoss):
|
|||||||
* 0.25: Avoiding trade loss
|
* 0.25: Avoiding trade loss
|
||||||
* 1.0 to total profit, compared to the expected value (`EXPECTED_MAX_PROFIT`) defined above
|
* 1.0 to total profit, compared to the expected value (`EXPECTED_MAX_PROFIT`) defined above
|
||||||
"""
|
"""
|
||||||
total_profit = results.profit_percent.sum()
|
total_profit = results['profit_percent'].sum()
|
||||||
trade_duration = results.trade_duration.mean()
|
trade_duration = results['trade_duration'].mean()
|
||||||
|
|
||||||
trade_loss = 1 - 0.25 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.8)
|
trade_loss = 1 - 0.25 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.8)
|
||||||
profit_loss = max(0, 1 - total_profit / EXPECTED_MAX_PROFIT)
|
profit_loss = max(0, 1 - total_profit / EXPECTED_MAX_PROFIT)
|
||||||
|
@ -272,7 +272,7 @@ the static list of pairs) if we should buy.
|
|||||||
|
|
||||||
### Understand order_types
|
### Understand order_types
|
||||||
|
|
||||||
The `order_types` configuration parameter maps actions (`buy`, `sell`, `stoploss`) to order-types (`market`, `limit`, ...) as well as configures stoploss to be on the exchange and defines stoploss on exchange update interval in seconds.
|
The `order_types` configuration parameter maps actions (`buy`, `sell`, `stoploss`, `emergencysell`) to order-types (`market`, `limit`, ...) as well as configures stoploss to be on the exchange and defines stoploss on exchange update interval in seconds.
|
||||||
|
|
||||||
This allows to buy using limit orders, sell using
|
This allows to buy using limit orders, sell using
|
||||||
limit-orders, and create stoplosses using using market orders. It also allows to set the
|
limit-orders, and create stoplosses using using market orders. It also allows to set the
|
||||||
@ -288,8 +288,12 @@ If this is configured, the following 4 values (`buy`, `sell`, `stoploss` and
|
|||||||
`emergencysell` is an optional value, which defaults to `market` and is used when creating stoploss on exchange orders fails.
|
`emergencysell` is an optional value, which defaults to `market` and is used when creating stoploss on exchange orders fails.
|
||||||
The below is the default which is used if this is not configured in either strategy or configuration file.
|
The below is the default which is used if this is not configured in either strategy or configuration file.
|
||||||
|
|
||||||
Since `stoploss_on_exchange` uses limit orders, the exchange needs 2 prices, the stoploss_price and the Limit price.
|
Not all Exchanges support `stoploss_on_exchange`. If an exchange supports both limit and market stoploss orders, then the value of `stoploss` will be used to determine the stoploss type.
|
||||||
`stoploss` defines the stop-price - and limit should be slightly below this. This defaults to 0.99 / 1% (configurable via `stoploss_on_exchange_limit_ratio`).
|
|
||||||
|
If `stoploss_on_exchange` uses limit orders, the exchange needs 2 prices, the stoploss_price and the Limit price.
|
||||||
|
`stoploss` defines the stop-price - and limit should be slightly below this.
|
||||||
|
|
||||||
|
This defaults to 0.99 / 1% (configurable via `stoploss_on_exchange_limit_ratio`).
|
||||||
Calculation example: we bought the asset at 100$.
|
Calculation example: we bought the asset at 100$.
|
||||||
Stop-price is 95$, then limit would be `95 * 0.99 = 94.05$` - so the stoploss will happen between 95$ and 94.05$.
|
Stop-price is 95$, then limit would be `95 * 0.99 = 94.05$` - so the stoploss will happen between 95$ and 94.05$.
|
||||||
|
|
||||||
@ -331,7 +335,10 @@ Configuration:
|
|||||||
refer to [the stoploss documentation](stoploss.md).
|
refer to [the stoploss documentation](stoploss.md).
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
If `stoploss_on_exchange` is enabled and the stoploss is cancelled manually on the exchange, then the bot will create a new order.
|
If `stoploss_on_exchange` is enabled and the stoploss is cancelled manually on the exchange, then the bot will create a new stoploss order.
|
||||||
|
|
||||||
|
!!! Warning "Using market orders"
|
||||||
|
Please read the section [Market order pricing](#market-order-pricing) section when using market orders.
|
||||||
|
|
||||||
!!! Warning "Warning: stoploss_on_exchange failures"
|
!!! Warning "Warning: stoploss_on_exchange failures"
|
||||||
If stoploss on exchange creation fails for some reason, then an "emergency sell" is initiated. By default, this will sell the asset using a market order. The order-type for the emergency-sell can be changed by setting the `emergencysell` value in the `order_types` dictionary - however this is not advised.
|
If stoploss on exchange creation fails for some reason, then an "emergency sell" is initiated. By default, this will sell the asset using a market order. The order-type for the emergency-sell can be changed by setting the `emergencysell` value in the `order_types` dictionary - however this is not advised.
|
||||||
@ -459,6 +466,9 @@ Prices are always retrieved right before an order is placed, either by querying
|
|||||||
!!! Note
|
!!! Note
|
||||||
Orderbook data used by Freqtrade are the data retrieved from exchange by the ccxt's function `fetch_order_book()`, i.e. are usually data from the L2-aggregated orderbook, while the ticker data are the structures returned by the ccxt's `fetch_ticker()`/`fetch_tickers()` functions. Refer to the ccxt library [documentation](https://github.com/ccxt/ccxt/wiki/Manual#market-data) for more details.
|
Orderbook data used by Freqtrade are the data retrieved from exchange by the ccxt's function `fetch_order_book()`, i.e. are usually data from the L2-aggregated orderbook, while the ticker data are the structures returned by the ccxt's `fetch_ticker()`/`fetch_tickers()` functions. Refer to the ccxt library [documentation](https://github.com/ccxt/ccxt/wiki/Manual#market-data) for more details.
|
||||||
|
|
||||||
|
!!! Warning "Using market orders"
|
||||||
|
Please read the section [Market order pricing](#market-order-pricing) section when using market orders.
|
||||||
|
|
||||||
### Buy price
|
### Buy price
|
||||||
|
|
||||||
#### Check depth of market
|
#### Check depth of market
|
||||||
@ -553,6 +563,29 @@ A fixed slot (mirroring `bid_strategy.order_book_top`) can be defined by setting
|
|||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
### Market order pricing
|
||||||
|
|
||||||
|
When using market orders, prices should be configured to use the "correct" side of the orderbook to allow realistic pricing detection.
|
||||||
|
Assuming both buy and sell are using market orders, a configuration similar to the following might be used
|
||||||
|
|
||||||
|
``` jsonc
|
||||||
|
"order_types": {
|
||||||
|
"buy": "market",
|
||||||
|
"sell": "market"
|
||||||
|
// ...
|
||||||
|
},
|
||||||
|
"bid_strategy": {
|
||||||
|
"price_side": "ask",
|
||||||
|
// ...
|
||||||
|
},
|
||||||
|
"ask_strategy":{
|
||||||
|
"price_side": "bid",
|
||||||
|
// ...
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
Obviously, if only one side is using limit orders, different pricing combinations can be used.
|
||||||
|
|
||||||
## Pairlists and Pairlist Handlers
|
## Pairlists and Pairlist Handlers
|
||||||
|
|
||||||
Pairlist Handlers define the list of pairs (pairlist) that the bot should trade. They are configured in the `pairlists` section of the configuration settings.
|
Pairlist Handlers define the list of pairs (pairlist) that the bot should trade. They are configured in the `pairlists` section of the configuration settings.
|
||||||
@ -591,7 +624,7 @@ It uses configuration from `exchange.pair_whitelist` and `exchange.pair_blacklis
|
|||||||
|
|
||||||
#### Volume Pair List
|
#### Volume Pair List
|
||||||
|
|
||||||
`VolumePairList` employs sorting/filtering of pairs by their trading volume. I selects `number_assets` top pairs with sorting based on the `sort_key` (which can only be `quoteVolume`).
|
`VolumePairList` employs sorting/filtering of pairs by their trading volume. It selects `number_assets` top pairs with sorting based on the `sort_key` (which can only be `quoteVolume`).
|
||||||
|
|
||||||
When used in the chain of Pairlist Handlers in a non-leading position (after StaticPairList and other Pairlist Filters), `VolumePairList` considers outputs of previous Pairlist Handlers, adding its sorting/selection of the pairs by the trading volume.
|
When used in the chain of Pairlist Handlers in a non-leading position (after StaticPairList and other Pairlist Filters), `VolumePairList` considers outputs of previous Pairlist Handlers, adding its sorting/selection of the pairs by the trading volume.
|
||||||
|
|
||||||
@ -609,7 +642,7 @@ The `refresh_period` setting allows to define the period (in seconds), at which
|
|||||||
"number_assets": 20,
|
"number_assets": 20,
|
||||||
"sort_key": "quoteVolume",
|
"sort_key": "quoteVolume",
|
||||||
"refresh_period": 1800,
|
"refresh_period": 1800,
|
||||||
],
|
}],
|
||||||
```
|
```
|
||||||
|
|
||||||
#### PrecisionFilter
|
#### PrecisionFilter
|
||||||
|
@ -30,6 +30,15 @@ Binance has been split into 3, and users must use the correct ccxt exchange ID f
|
|||||||
The Kraken API does only provide 720 historic candles, which is sufficient for Freqtrade dry-run and live trade modes, but is a problem for backtesting.
|
The Kraken API does only provide 720 historic candles, which is sufficient for Freqtrade dry-run and live trade modes, but is a problem for backtesting.
|
||||||
To download data for the Kraken exchange, using `--dl-trades` is mandatory, otherwise the bot will download the same 720 candles over and over, and you'll not have enough backtest data.
|
To download data for the Kraken exchange, using `--dl-trades` is mandatory, otherwise the bot will download the same 720 candles over and over, and you'll not have enough backtest data.
|
||||||
|
|
||||||
|
Due to the heavy rate-limiting applied by Kraken, the following configuration section should be used to download data:
|
||||||
|
|
||||||
|
``` json
|
||||||
|
"ccxt_async_config": {
|
||||||
|
"enableRateLimit": true,
|
||||||
|
"rateLimit": 3100
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
## Bittrex
|
## Bittrex
|
||||||
|
|
||||||
### Order types
|
### Order types
|
||||||
@ -64,6 +73,11 @@ print(res)
|
|||||||
|
|
||||||
## FTX
|
## FTX
|
||||||
|
|
||||||
|
!!! Tip "Stoploss on Exchange"
|
||||||
|
FTX 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.
|
||||||
|
|
||||||
|
|
||||||
### Using subaccounts
|
### Using subaccounts
|
||||||
|
|
||||||
To use subaccounts with FTX, you need to edit the configuration and add the following:
|
To use subaccounts with FTX, you need to edit the configuration and add the following:
|
||||||
|
@ -265,7 +265,7 @@ freqtrade hyperopt --timerange 20180401-20180501
|
|||||||
Hyperopt can reuse `populate_indicators`, `populate_buy_trend`, `populate_sell_trend` from your strategy, assuming these methods are **not** in your custom hyperopt file, and a strategy is provided.
|
Hyperopt can reuse `populate_indicators`, `populate_buy_trend`, `populate_sell_trend` from your strategy, assuming these methods are **not** in your custom hyperopt file, and a strategy is provided.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
freqtrade hyperopt --strategy SampleStrategy --customhyperopt SampleHyperopt
|
freqtrade hyperopt --strategy SampleStrategy --hyperopt SampleHyperopt
|
||||||
```
|
```
|
||||||
|
|
||||||
### Running Hyperopt with Smaller Search Space
|
### Running Hyperopt with Smaller Search Space
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
mkdocs-material==5.2.2
|
mkdocs-material==5.2.3
|
||||||
mdx_truly_sane_lists==1.2
|
mdx_truly_sane_lists==1.2
|
||||||
|
@ -110,7 +110,7 @@ python3 scripts/rest_client.py --config rest_config.json <command> [optional par
|
|||||||
| `start` | | Starts the trader
|
| `start` | | Starts the trader
|
||||||
| `stop` | | Stops the trader
|
| `stop` | | Stops the trader
|
||||||
| `stopbuy` | | Stops the trader from opening new trades. Gracefully closes open trades according to their rules.
|
| `stopbuy` | | Stops the trader from opening new trades. Gracefully closes open trades according to their rules.
|
||||||
| `reload_conf` | | Reloads the configuration file
|
| `reload_config` | | Reloads the configuration file
|
||||||
| `show_config` | | Shows part of the current configuration with relevant settings to operation
|
| `show_config` | | Shows part of the current configuration with relevant settings to operation
|
||||||
| `status` | | Lists all open trades
|
| `status` | | Lists all open trades
|
||||||
| `count` | | Displays number of trades used and available
|
| `count` | | Displays number of trades used and available
|
||||||
@ -174,7 +174,7 @@ profit
|
|||||||
Returns the profit summary
|
Returns the profit summary
|
||||||
:returns: json object
|
:returns: json object
|
||||||
|
|
||||||
reload_conf
|
reload_config
|
||||||
Reload configuration
|
Reload configuration
|
||||||
:returns: json object
|
:returns: json object
|
||||||
|
|
||||||
@ -196,7 +196,7 @@ stop
|
|||||||
|
|
||||||
stopbuy
|
stopbuy
|
||||||
Stop buying (but handle sells gracefully).
|
Stop buying (but handle sells gracefully).
|
||||||
use reload_conf to reset
|
use reload_config to reset
|
||||||
:returns: json object
|
:returns: json object
|
||||||
|
|
||||||
version
|
version
|
||||||
|
@ -101,7 +101,7 @@ SET is_open=0,
|
|||||||
close_date=<close_date>,
|
close_date=<close_date>,
|
||||||
close_rate=<close_rate>,
|
close_rate=<close_rate>,
|
||||||
close_profit=close_rate/open_rate-1,
|
close_profit=close_rate/open_rate-1,
|
||||||
close_profit_abs = (amount * <close_rate> * (1 - fee_close) - (amount * open_rate * 1 - fee_open),
|
close_profit_abs = (amount * <close_rate> * (1 - fee_close) - (amount * open_rate * 1 - fee_open)),
|
||||||
sell_reason=<sell_reason>
|
sell_reason=<sell_reason>
|
||||||
WHERE id=<trade_ID_to_update>;
|
WHERE id=<trade_ID_to_update>;
|
||||||
```
|
```
|
||||||
@ -114,7 +114,7 @@ SET is_open=0,
|
|||||||
close_date='2017-12-20 03:08:45.103418',
|
close_date='2017-12-20 03:08:45.103418',
|
||||||
close_rate=0.19638016,
|
close_rate=0.19638016,
|
||||||
close_profit=0.0496,
|
close_profit=0.0496,
|
||||||
close_profit_abs = (amount * 0.19638016 * (1 - fee_close) - (amount * open_rate * 1 - fee_open)
|
close_profit_abs = (amount * 0.19638016 * (1 - fee_close) - (amount * open_rate * 1 - fee_open))
|
||||||
sell_reason='force_sell'
|
sell_reason='force_sell'
|
||||||
WHERE id=31;
|
WHERE id=31;
|
||||||
```
|
```
|
||||||
|
@ -27,7 +27,7 @@ So this parameter will tell the bot how often it should update the stoploss orde
|
|||||||
This same logic will reapply a stoploss order on the exchange should you cancel it accidentally.
|
This same logic will reapply a stoploss order on the exchange should you cancel it accidentally.
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
Stoploss on exchange is only supported for Binance (stop-loss-limit) and Kraken (stop-loss-market) as of now.
|
Stoploss on exchange is only supported for Binance (stop-loss-limit), Kraken (stop-loss-market) and FTX (stop limit and stop-market) as of now.
|
||||||
|
|
||||||
## Static Stop Loss
|
## Static Stop Loss
|
||||||
|
|
||||||
@ -101,7 +101,7 @@ Simplified example:
|
|||||||
|
|
||||||
## Changing stoploss on open trades
|
## Changing stoploss on open trades
|
||||||
|
|
||||||
A stoploss on an open trade can be changed by changing the value in the configuration or strategy and use the `/reload_conf` command (alternatively, completely stopping and restarting the bot also works).
|
A stoploss on an open trade can be changed by changing the value in the configuration or strategy and use the `/reload_config` command (alternatively, completely stopping and restarting the bot also works).
|
||||||
|
|
||||||
The new stoploss value will be applied to open trades (and corresponding log-messages will be generated).
|
The new stoploss value will be applied to open trades (and corresponding log-messages will be generated).
|
||||||
|
|
||||||
|
@ -139,7 +139,7 @@ By letting the bot know how much history is needed, backtest trades can start at
|
|||||||
|
|
||||||
#### Example
|
#### Example
|
||||||
|
|
||||||
Let's try to backtest 1 month (January 2019) of 5m candles using the an example strategy with EMA100, as above.
|
Let's try to backtest 1 month (January 2019) of 5m candles using an example strategy with EMA100, as above.
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
freqtrade backtesting --timerange 20190101-20190201 --timeframe 5m
|
freqtrade backtesting --timerange 20190101-20190201 --timeframe 5m
|
||||||
@ -557,7 +557,7 @@ Locks can also be lifted manually, by calling `self.unlock_pair(pair)`.
|
|||||||
To verify if a pair is currently locked, use `self.is_pair_locked(pair)`.
|
To verify if a pair is currently locked, use `self.is_pair_locked(pair)`.
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
Locked pairs are not persisted, so a restart of the bot, or calling `/reload_conf` will reset locked pairs.
|
Locked pairs are not persisted, so a restart of the bot, or calling `/reload_config` will reset locked pairs.
|
||||||
|
|
||||||
!!! Warning
|
!!! Warning
|
||||||
Locking pairs is not functioning during backtesting.
|
Locking pairs is not functioning during backtesting.
|
||||||
|
@ -52,7 +52,7 @@ official commands. You can ask at any moment for help with `/help`.
|
|||||||
| `/start` | | Starts the trader
|
| `/start` | | Starts the trader
|
||||||
| `/stop` | | Stops the trader
|
| `/stop` | | Stops the trader
|
||||||
| `/stopbuy` | | Stops the trader from opening new trades. Gracefully closes open trades according to their rules.
|
| `/stopbuy` | | Stops the trader from opening new trades. Gracefully closes open trades according to their rules.
|
||||||
| `/reload_conf` | | Reloads the configuration file
|
| `/reload_config` | | Reloads the configuration file
|
||||||
| `/show_config` | | Shows part of the current configuration with relevant settings to operation
|
| `/show_config` | | Shows part of the current configuration with relevant settings to operation
|
||||||
| `/status` | | Lists all open trades
|
| `/status` | | Lists all open trades
|
||||||
| `/status table` | | List all open trades in a table format. Pending buy orders are marked with an asterisk (*) Pending sell orders are marked with a double asterisk (**)
|
| `/status table` | | List all open trades in a table format. Pending buy orders are marked with an asterisk (*) Pending sell orders are marked with a double asterisk (**)
|
||||||
@ -85,14 +85,14 @@ Below, example of Telegram message you will receive for each command.
|
|||||||
|
|
||||||
### /stopbuy
|
### /stopbuy
|
||||||
|
|
||||||
> **status:** `Setting max_open_trades to 0. Run /reload_conf to reset.`
|
> **status:** `Setting max_open_trades to 0. Run /reload_config to reset.`
|
||||||
|
|
||||||
Prevents the bot from opening new trades by temporarily setting "max_open_trades" to 0. Open trades will be handled via their regular rules (ROI / Sell-signal, stoploss, ...).
|
Prevents the bot from opening new trades by temporarily setting "max_open_trades" to 0. Open trades will be handled via their regular rules (ROI / Sell-signal, stoploss, ...).
|
||||||
|
|
||||||
After this, give the bot time to close off open trades (can be checked via `/status table`).
|
After this, give the bot time to close off open trades (can be checked via `/status table`).
|
||||||
Once all positions are sold, run `/stop` to completely stop the bot.
|
Once all positions are sold, run `/stop` to completely stop the bot.
|
||||||
|
|
||||||
`/reload_conf` resets "max_open_trades" to the value set in the configuration and resets this command.
|
`/reload_config` resets "max_open_trades" to the value set in the configuration and resets this command.
|
||||||
|
|
||||||
!!! Warning
|
!!! Warning
|
||||||
The stop-buy signal is ONLY active while the bot is running, and is not persisted anyway, so restarting the bot will cause this to reset.
|
The stop-buy signal is ONLY active while the bot is running, and is not persisted anyway, so restarting the bot will cause this to reset.
|
||||||
@ -209,7 +209,7 @@ Shows the current whitelist
|
|||||||
Shows the current blacklist.
|
Shows the current blacklist.
|
||||||
If Pair is set, then this pair will be added to the pairlist.
|
If Pair is set, then this pair will be added to the pairlist.
|
||||||
Also supports multiple pairs, seperated by a space.
|
Also supports multiple pairs, seperated by a space.
|
||||||
Use `/reload_conf` to reset the blacklist.
|
Use `/reload_config` to reset the blacklist.
|
||||||
|
|
||||||
> Using blacklist `StaticPairList` with 2 pairs
|
> Using blacklist `StaticPairList` with 2 pairs
|
||||||
>`DODGE/BTC`, `HOT/BTC`.
|
>`DODGE/BTC`, `HOT/BTC`.
|
||||||
|
@ -16,7 +16,7 @@ from freqtrade.persistence import Trade
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# must align with columns in backtest.py
|
# must align with columns in backtest.py
|
||||||
BT_DATA_COLUMNS = ["pair", "profitperc", "open_time", "close_time", "index", "duration",
|
BT_DATA_COLUMNS = ["pair", "profit_percent", "open_time", "close_time", "index", "duration",
|
||||||
"open_rate", "close_rate", "open_at_end", "sell_reason"]
|
"open_rate", "close_rate", "open_at_end", "sell_reason"]
|
||||||
|
|
||||||
|
|
||||||
@ -99,7 +99,7 @@ def load_trades_from_db(db_url: str) -> pd.DataFrame:
|
|||||||
trades: pd.DataFrame = pd.DataFrame([], columns=BT_DATA_COLUMNS)
|
trades: pd.DataFrame = pd.DataFrame([], columns=BT_DATA_COLUMNS)
|
||||||
persistence.init(db_url, clean_open_orders=False)
|
persistence.init(db_url, clean_open_orders=False)
|
||||||
|
|
||||||
columns = ["pair", "open_time", "close_time", "profit", "profitperc",
|
columns = ["pair", "open_time", "close_time", "profit", "profit_percent",
|
||||||
"open_rate", "close_rate", "amount", "duration", "sell_reason",
|
"open_rate", "close_rate", "amount", "duration", "sell_reason",
|
||||||
"fee_open", "fee_close", "open_rate_requested", "close_rate_requested",
|
"fee_open", "fee_close", "open_rate_requested", "close_rate_requested",
|
||||||
"stake_amount", "max_rate", "min_rate", "id", "exchange",
|
"stake_amount", "max_rate", "min_rate", "id", "exchange",
|
||||||
@ -190,7 +190,7 @@ def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str,
|
|||||||
"""
|
"""
|
||||||
Adds a column `col_name` with the cumulative profit for the given trades array.
|
Adds a column `col_name` with the cumulative profit for the given trades array.
|
||||||
:param df: DataFrame with date index
|
:param df: DataFrame with date index
|
||||||
:param trades: DataFrame containing trades (requires columns close_time and profitperc)
|
:param trades: DataFrame containing trades (requires columns close_time and profit_percent)
|
||||||
:param col_name: Column name that will be assigned the results
|
:param col_name: Column name that will be assigned the results
|
||||||
:param timeframe: Timeframe used during the operations
|
:param timeframe: Timeframe used during the operations
|
||||||
:return: Returns df with one additional column, col_name, containing the cumulative profit.
|
:return: Returns df with one additional column, col_name, containing the cumulative profit.
|
||||||
@ -201,7 +201,8 @@ def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str,
|
|||||||
from freqtrade.exchange import timeframe_to_minutes
|
from freqtrade.exchange import timeframe_to_minutes
|
||||||
timeframe_minutes = timeframe_to_minutes(timeframe)
|
timeframe_minutes = timeframe_to_minutes(timeframe)
|
||||||
# Resample to timeframe to make sure trades match candles
|
# Resample to timeframe to make sure trades match candles
|
||||||
_trades_sum = trades.resample(f'{timeframe_minutes}min', on='close_time')[['profitperc']].sum()
|
_trades_sum = trades.resample(f'{timeframe_minutes}min', on='close_time'
|
||||||
|
)[['profit_percent']].sum()
|
||||||
df.loc[:, col_name] = _trades_sum.cumsum()
|
df.loc[:, col_name] = _trades_sum.cumsum()
|
||||||
# Set first value to 0
|
# Set first value to 0
|
||||||
df.loc[df.iloc[0].name, col_name] = 0
|
df.loc[df.iloc[0].name, col_name] = 0
|
||||||
@ -211,13 +212,13 @@ def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str,
|
|||||||
|
|
||||||
|
|
||||||
def calculate_max_drawdown(trades: pd.DataFrame, *, date_col: str = 'close_time',
|
def calculate_max_drawdown(trades: pd.DataFrame, *, date_col: str = 'close_time',
|
||||||
value_col: str = 'profitperc'
|
value_col: str = 'profit_percent'
|
||||||
) -> Tuple[float, pd.Timestamp, pd.Timestamp]:
|
) -> Tuple[float, pd.Timestamp, pd.Timestamp]:
|
||||||
"""
|
"""
|
||||||
Calculate max drawdown and the corresponding close dates
|
Calculate max drawdown and the corresponding close dates
|
||||||
:param trades: DataFrame containing trades (requires columns close_time and profitperc)
|
:param trades: DataFrame containing trades (requires columns close_time and profit_percent)
|
||||||
:param date_col: Column in DataFrame to use for dates (defaults to 'close_time')
|
:param date_col: Column in DataFrame to use for dates (defaults to 'close_time')
|
||||||
:param value_col: Column in DataFrame to use for values (defaults to 'profitperc')
|
:param value_col: Column in DataFrame to use for values (defaults to 'profit_percent')
|
||||||
:return: Tuple (float, highdate, lowdate) with absolute max drawdown, high and low time
|
:return: Tuple (float, highdate, lowdate) with absolute max drawdown, high and low time
|
||||||
:raise: ValueError if trade-dataframe was found empty.
|
:raise: ValueError if trade-dataframe was found empty.
|
||||||
"""
|
"""
|
||||||
|
@ -197,7 +197,7 @@ def trades_to_ohlcv(trades: List, timeframe: str) -> DataFrame:
|
|||||||
df_new['date'] = df_new.index
|
df_new['date'] = df_new.index
|
||||||
# Drop 0 volume rows
|
# Drop 0 volume rows
|
||||||
df_new = df_new.dropna()
|
df_new = df_new.dropna()
|
||||||
return df_new[DEFAULT_DATAFRAME_COLUMNS]
|
return df_new.loc[:, DEFAULT_DATAFRAME_COLUMNS]
|
||||||
|
|
||||||
|
|
||||||
def convert_trades_format(config: Dict[str, Any], convert_from: str, convert_to: str, erase: bool):
|
def convert_trades_format(config: Dict[str, Any], convert_from: str, convert_to: str, erase: bool):
|
||||||
|
@ -79,7 +79,7 @@ class Exchange:
|
|||||||
|
|
||||||
if config['dry_run']:
|
if config['dry_run']:
|
||||||
logger.info('Instance is running with dry_run enabled')
|
logger.info('Instance is running with dry_run enabled')
|
||||||
|
logger.info(f"Using CCXT {ccxt.__version__}")
|
||||||
exchange_config = config['exchange']
|
exchange_config = config['exchange']
|
||||||
|
|
||||||
# Deep merge ft_has with default ft_has options
|
# Deep merge ft_has with default ft_has options
|
||||||
@ -190,7 +190,7 @@ class Exchange:
|
|||||||
def markets(self) -> Dict:
|
def markets(self) -> Dict:
|
||||||
"""exchange ccxt markets"""
|
"""exchange ccxt markets"""
|
||||||
if not self._api.markets:
|
if not self._api.markets:
|
||||||
logger.warning("Markets were not loaded. Loading them now..")
|
logger.info("Markets were not loaded. Loading them now..")
|
||||||
self._load_markets()
|
self._load_markets()
|
||||||
return self._api.markets
|
return self._api.markets
|
||||||
|
|
||||||
@ -275,8 +275,8 @@ class Exchange:
|
|||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
logger.warning('Unable to initialize markets. Reason: %s', e)
|
logger.warning('Unable to initialize markets. Reason: %s', e)
|
||||||
|
|
||||||
def _reload_markets(self) -> None:
|
def reload_markets(self) -> None:
|
||||||
"""Reload markets both sync and async, if refresh interval has passed"""
|
"""Reload markets both sync and async if refresh interval has passed """
|
||||||
# Check whether markets have to be reloaded
|
# Check whether markets have to be reloaded
|
||||||
if (self._last_markets_refresh > 0) and (
|
if (self._last_markets_refresh > 0) and (
|
||||||
self._last_markets_refresh + self.markets_refresh_interval
|
self._last_markets_refresh + self.markets_refresh_interval
|
||||||
@ -889,14 +889,19 @@ class Exchange:
|
|||||||
Async wrapper handling downloading trades using either time or id based methods.
|
Async wrapper handling downloading trades using either time or id based methods.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
logger.debug(f"_async_get_trade_history(), pair: {pair}, "
|
||||||
|
f"since: {since}, until: {until}, from_id: {from_id}")
|
||||||
|
|
||||||
|
if until is None:
|
||||||
|
until = ccxt.Exchange.milliseconds()
|
||||||
|
logger.debug(f"Exchange milliseconds: {until}")
|
||||||
|
|
||||||
if self._trades_pagination == 'time':
|
if self._trades_pagination == 'time':
|
||||||
return await self._async_get_trade_history_time(
|
return await self._async_get_trade_history_time(
|
||||||
pair=pair, since=since,
|
pair=pair, since=since, until=until)
|
||||||
until=until or ccxt.Exchange.milliseconds())
|
|
||||||
elif self._trades_pagination == 'id':
|
elif self._trades_pagination == 'id':
|
||||||
return await self._async_get_trade_history_id(
|
return await self._async_get_trade_history_id(
|
||||||
pair=pair, since=since,
|
pair=pair, since=since, until=until, from_id=from_id
|
||||||
until=until or ccxt.Exchange.milliseconds(), from_id=from_id
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise OperationalException(f"Exchange {self.name} does use neither time, "
|
raise OperationalException(f"Exchange {self.name} does use neither time, "
|
||||||
@ -947,6 +952,9 @@ class Exchange:
|
|||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e) from e
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
|
# Assign method to get_stoploss_order to allow easy overriding in other classes
|
||||||
|
cancel_stoploss_order = cancel_order
|
||||||
|
|
||||||
def is_cancel_order_result_suitable(self, corder) -> bool:
|
def is_cancel_order_result_suitable(self, corder) -> bool:
|
||||||
if not isinstance(corder, dict):
|
if not isinstance(corder, dict):
|
||||||
return False
|
return False
|
||||||
@ -999,6 +1007,9 @@ class Exchange:
|
|||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e) from e
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
|
# Assign method to get_stoploss_order to allow easy overriding in other classes
|
||||||
|
get_stoploss_order = get_order
|
||||||
|
|
||||||
@retrier
|
@retrier
|
||||||
def fetch_l2_order_book(self, pair: str, limit: int = 100) -> dict:
|
def fetch_l2_order_book(self, pair: str, limit: int = 100) -> dict:
|
||||||
"""
|
"""
|
||||||
@ -1104,9 +1115,12 @@ class Exchange:
|
|||||||
order['fee']['cost'] / safe_value_fallback(order, order, 'filled', 'amount'), 8)
|
order['fee']['cost'] / safe_value_fallback(order, order, 'filled', 'amount'), 8)
|
||||||
elif fee_curr in self.get_pair_quote_currency(order['symbol']):
|
elif fee_curr in self.get_pair_quote_currency(order['symbol']):
|
||||||
# Quote currency - divide by cost
|
# Quote currency - divide by cost
|
||||||
return round(order['fee']['cost'] / order['cost'], 8)
|
return round(order['fee']['cost'] / order['cost'], 8) if order['cost'] else None
|
||||||
else:
|
else:
|
||||||
# If Fee currency is a different currency
|
# If Fee currency is a different currency
|
||||||
|
if not order['cost']:
|
||||||
|
# If cost is None or 0.0 -> falsy, return None
|
||||||
|
return None
|
||||||
try:
|
try:
|
||||||
comb = self.get_valid_pair_combination(fee_curr, self._config['stake_currency'])
|
comb = self.get_valid_pair_combination(fee_curr, self._config['stake_currency'])
|
||||||
tick = self.fetch_ticker(comb)
|
tick = self.fetch_ticker(comb)
|
||||||
|
@ -2,7 +2,12 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
|
||||||
|
import ccxt
|
||||||
|
|
||||||
|
from freqtrade.exceptions import (DependencyException, 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__)
|
||||||
|
|
||||||
@ -10,5 +15,104 @@ logger = logging.getLogger(__name__)
|
|||||||
class Ftx(Exchange):
|
class Ftx(Exchange):
|
||||||
|
|
||||||
_ft_has: Dict = {
|
_ft_has: Dict = {
|
||||||
|
"stoploss_on_exchange": True,
|
||||||
"ohlcv_candle_limit": 1500,
|
"ohlcv_candle_limit": 1500,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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['price'])
|
||||||
|
|
||||||
|
def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict:
|
||||||
|
"""
|
||||||
|
Creates a stoploss order.
|
||||||
|
depending on order_types.stoploss configuration, uses 'market' or limit order.
|
||||||
|
|
||||||
|
Limit orders are defined by having orderPrice set, otherwise a market order is used.
|
||||||
|
"""
|
||||||
|
limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99)
|
||||||
|
limit_rate = stop_price * limit_price_pct
|
||||||
|
|
||||||
|
ordertype = "stop"
|
||||||
|
|
||||||
|
stop_price = self.price_to_precision(pair, stop_price)
|
||||||
|
|
||||||
|
if self._config['dry_run']:
|
||||||
|
dry_order = self.dry_run_order(
|
||||||
|
pair, ordertype, "sell", amount, stop_price)
|
||||||
|
return dry_order
|
||||||
|
|
||||||
|
try:
|
||||||
|
params = self._params.copy()
|
||||||
|
if order_types.get('stoploss', 'market') == 'limit':
|
||||||
|
# set orderPrice to place limit order, otherwise it's a market order
|
||||||
|
params['orderPrice'] = limit_rate
|
||||||
|
|
||||||
|
amount = self.amount_to_precision(pair, amount)
|
||||||
|
|
||||||
|
order = self._api.create_order(symbol=pair, type=ordertype, side='sell',
|
||||||
|
amount=amount, price=stop_price, params=params)
|
||||||
|
logger.info('stoploss order added for %s. '
|
||||||
|
'stop price: %s.', pair, stop_price)
|
||||||
|
return order
|
||||||
|
except ccxt.InsufficientFunds as e:
|
||||||
|
raise DependencyException(
|
||||||
|
f'Insufficient funds to create {ordertype} sell order on market {pair}. '
|
||||||
|
f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. '
|
||||||
|
f'Message: {e}') from e
|
||||||
|
except ccxt.InvalidOrder as e:
|
||||||
|
raise InvalidOrderException(
|
||||||
|
f'Could not create {ordertype} sell order on market {pair}. '
|
||||||
|
f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. '
|
||||||
|
f'Message: {e}') from e
|
||||||
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
|
raise TemporaryError(
|
||||||
|
f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e
|
||||||
|
except ccxt.BaseError as e:
|
||||||
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
|
@retrier
|
||||||
|
def get_stoploss_order(self, order_id: str, pair: str) -> Dict:
|
||||||
|
if self._config['dry_run']:
|
||||||
|
try:
|
||||||
|
order = self._dry_run_open_orders[order_id]
|
||||||
|
return order
|
||||||
|
except KeyError as e:
|
||||||
|
# Gracefully handle errors with dry-run orders.
|
||||||
|
raise InvalidOrderException(
|
||||||
|
f'Tried to get an invalid dry-run-order (id: {order_id}). Message: {e}') from e
|
||||||
|
try:
|
||||||
|
orders = self._api.fetch_orders(pair, None, params={'type': 'stop'})
|
||||||
|
|
||||||
|
order = [order for order in orders if order['id'] == order_id]
|
||||||
|
if len(order) == 1:
|
||||||
|
return order[0]
|
||||||
|
else:
|
||||||
|
raise InvalidOrderException(f"Could not get stoploss order for id {order_id}")
|
||||||
|
|
||||||
|
except ccxt.InvalidOrder as e:
|
||||||
|
raise InvalidOrderException(
|
||||||
|
f'Tried to get an invalid order (id: {order_id}). Message: {e}') from e
|
||||||
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
|
raise TemporaryError(
|
||||||
|
f'Could not get order due to {e.__class__.__name__}. Message: {e}') from e
|
||||||
|
except ccxt.BaseError as e:
|
||||||
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
|
@retrier
|
||||||
|
def cancel_stoploss_order(self, order_id: str, pair: str) -> Dict:
|
||||||
|
if self._config['dry_run']:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
return self._api.cancel_order(order_id, pair, params={'type': 'stop'})
|
||||||
|
except ccxt.InvalidOrder as e:
|
||||||
|
raise InvalidOrderException(
|
||||||
|
f'Could not cancel order. Message: {e}') from e
|
||||||
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
|
raise TemporaryError(
|
||||||
|
f'Could not cancel order due to {e.__class__.__name__}. Message: {e}') from e
|
||||||
|
except ccxt.BaseError as e:
|
||||||
|
raise OperationalException(e) from e
|
||||||
|
@ -139,8 +139,8 @@ class FreqtradeBot:
|
|||||||
:return: True if one or more trades has been created or closed, False otherwise
|
:return: True if one or more trades has been created or closed, False otherwise
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Check whether markets have to be reloaded
|
# Check whether markets have to be reloaded and reload them when it's needed
|
||||||
self.exchange._reload_markets()
|
self.exchange.reload_markets()
|
||||||
|
|
||||||
# Query trades from persistence layer
|
# Query trades from persistence layer
|
||||||
trades = Trade.get_open_trades()
|
trades = Trade.get_open_trades()
|
||||||
@ -702,10 +702,9 @@ class FreqtradeBot:
|
|||||||
self.dataprovider.ohlcv(trade.pair, self.strategy.timeframe))
|
self.dataprovider.ohlcv(trade.pair, self.strategy.timeframe))
|
||||||
|
|
||||||
if config_ask_strategy.get('use_order_book', False):
|
if config_ask_strategy.get('use_order_book', False):
|
||||||
# logger.debug('Order book %s',orderBook)
|
|
||||||
order_book_min = config_ask_strategy.get('order_book_min', 1)
|
order_book_min = config_ask_strategy.get('order_book_min', 1)
|
||||||
order_book_max = config_ask_strategy.get('order_book_max', 1)
|
order_book_max = config_ask_strategy.get('order_book_max', 1)
|
||||||
logger.info(f'Using order book between {order_book_min} and {order_book_max} '
|
logger.debug(f'Using order book between {order_book_min} and {order_book_max} '
|
||||||
f'for selling {trade.pair}...')
|
f'for selling {trade.pair}...')
|
||||||
|
|
||||||
order_book = self._order_book_gen(trade.pair, f"{config_ask_strategy['price_side']}s",
|
order_book = self._order_book_gen(trade.pair, f"{config_ask_strategy['price_side']}s",
|
||||||
@ -774,13 +773,13 @@ class FreqtradeBot:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# First we check if there is already a stoploss on exchange
|
# First we check if there is already a stoploss on exchange
|
||||||
stoploss_order = self.exchange.get_order(trade.stoploss_order_id, trade.pair) \
|
stoploss_order = self.exchange.get_stoploss_order(trade.stoploss_order_id, trade.pair) \
|
||||||
if trade.stoploss_order_id else None
|
if trade.stoploss_order_id else None
|
||||||
except InvalidOrderException as exception:
|
except InvalidOrderException as exception:
|
||||||
logger.warning('Unable to fetch stoploss order: %s', exception)
|
logger.warning('Unable to fetch stoploss order: %s', exception)
|
||||||
|
|
||||||
# We check if stoploss order is fulfilled
|
# We check if stoploss order is fulfilled
|
||||||
if stoploss_order and stoploss_order['status'] == 'closed':
|
if stoploss_order and stoploss_order['status'] in ('closed', 'triggered'):
|
||||||
trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value
|
trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value
|
||||||
self.update_trade_state(trade, stoploss_order, sl_order=True)
|
self.update_trade_state(trade, stoploss_order, sl_order=True)
|
||||||
# Lock pair for one candle to prevent immediate rebuys
|
# Lock pair for one candle to prevent immediate rebuys
|
||||||
@ -807,7 +806,7 @@ class FreqtradeBot:
|
|||||||
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'] == 'canceled':
|
if 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,
|
||||||
rate=trade.stop_loss):
|
rate=trade.stop_loss):
|
||||||
return False
|
return False
|
||||||
@ -840,7 +839,7 @@ class FreqtradeBot:
|
|||||||
logger.info('Trailing stoploss: cancelling current stoploss on exchange (id:{%s}) '
|
logger.info('Trailing stoploss: cancelling current stoploss on exchange (id:{%s}) '
|
||||||
'in order to add another one ...', order['id'])
|
'in order to add another one ...', order['id'])
|
||||||
try:
|
try:
|
||||||
self.exchange.cancel_order(order['id'], trade.pair)
|
self.exchange.cancel_stoploss_order(order['id'], trade.pair)
|
||||||
except InvalidOrderException:
|
except InvalidOrderException:
|
||||||
logger.exception(f"Could not cancel stoploss order {order['id']} "
|
logger.exception(f"Could not cancel stoploss order {order['id']} "
|
||||||
f"for pair {trade.pair}")
|
f"for pair {trade.pair}")
|
||||||
@ -1068,7 +1067,7 @@ class FreqtradeBot:
|
|||||||
# First cancelling stoploss on exchange ...
|
# First cancelling stoploss on exchange ...
|
||||||
if self.strategy.order_types.get('stoploss_on_exchange') and trade.stoploss_order_id:
|
if self.strategy.order_types.get('stoploss_on_exchange') and trade.stoploss_order_id:
|
||||||
try:
|
try:
|
||||||
self.exchange.cancel_order(trade.stoploss_order_id, trade.pair)
|
self.exchange.cancel_stoploss_order(trade.stoploss_order_id, trade.pair)
|
||||||
except InvalidOrderException:
|
except InvalidOrderException:
|
||||||
logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}")
|
logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}")
|
||||||
|
|
||||||
|
@ -42,8 +42,8 @@ class DefaultHyperOptLoss(IHyperOptLoss):
|
|||||||
* 0.25: Avoiding trade loss
|
* 0.25: Avoiding trade loss
|
||||||
* 1.0 to total profit, compared to the expected value (`EXPECTED_MAX_PROFIT`) defined above
|
* 1.0 to total profit, compared to the expected value (`EXPECTED_MAX_PROFIT`) defined above
|
||||||
"""
|
"""
|
||||||
total_profit = results.profit_percent.sum()
|
total_profit = results['profit_percent'].sum()
|
||||||
trade_duration = results.trade_duration.mean()
|
trade_duration = results['trade_duration'].mean()
|
||||||
|
|
||||||
trade_loss = 1 - 0.25 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.8)
|
trade_loss = 1 - 0.25 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.8)
|
||||||
profit_loss = max(0, 1 - total_profit / EXPECTED_MAX_PROFIT)
|
profit_loss = max(0, 1 - total_profit / EXPECTED_MAX_PROFIT)
|
||||||
|
@ -34,5 +34,5 @@ class OnlyProfitHyperOptLoss(IHyperOptLoss):
|
|||||||
"""
|
"""
|
||||||
Objective function, returns smaller number for better results.
|
Objective function, returns smaller number for better results.
|
||||||
"""
|
"""
|
||||||
total_profit = results.profit_percent.sum()
|
total_profit = results['profit_percent'].sum()
|
||||||
return 1 - total_profit / EXPECTED_MAX_PROFIT
|
return 1 - total_profit / EXPECTED_MAX_PROFIT
|
||||||
|
@ -65,25 +65,25 @@ def _generate_result_line(result: DataFrame, max_open_trades: int, first_column:
|
|||||||
"""
|
"""
|
||||||
return {
|
return {
|
||||||
'key': first_column,
|
'key': first_column,
|
||||||
'trades': len(result.index),
|
'trades': len(result),
|
||||||
'profit_mean': result.profit_percent.mean(),
|
'profit_mean': result['profit_percent'].mean(),
|
||||||
'profit_mean_pct': result.profit_percent.mean() * 100.0,
|
'profit_mean_pct': result['profit_percent'].mean() * 100.0,
|
||||||
'profit_sum': result.profit_percent.sum(),
|
'profit_sum': result['profit_percent'].sum(),
|
||||||
'profit_sum_pct': result.profit_percent.sum() * 100.0,
|
'profit_sum_pct': result['profit_percent'].sum() * 100.0,
|
||||||
'profit_total_abs': result.profit_abs.sum(),
|
'profit_total_abs': result['profit_abs'].sum(),
|
||||||
'profit_total_pct': result.profit_percent.sum() * 100.0 / max_open_trades,
|
'profit_total_pct': result['profit_percent'].sum() * 100.0 / max_open_trades,
|
||||||
'duration_avg': str(timedelta(
|
'duration_avg': str(timedelta(
|
||||||
minutes=round(result.trade_duration.mean()))
|
minutes=round(result['trade_duration'].mean()))
|
||||||
) if not result.empty else '0:00',
|
) if not result.empty else '0:00',
|
||||||
# 'duration_max': str(timedelta(
|
# 'duration_max': str(timedelta(
|
||||||
# minutes=round(result.trade_duration.max()))
|
# minutes=round(result['trade_duration'].max()))
|
||||||
# ) if not result.empty else '0:00',
|
# ) if not result.empty else '0:00',
|
||||||
# 'duration_min': str(timedelta(
|
# 'duration_min': str(timedelta(
|
||||||
# minutes=round(result.trade_duration.min()))
|
# minutes=round(result['trade_duration'].min()))
|
||||||
# ) if not result.empty else '0:00',
|
# ) if not result.empty else '0:00',
|
||||||
'wins': len(result[result.profit_abs > 0]),
|
'wins': len(result[result['profit_abs'] > 0]),
|
||||||
'draws': len(result[result.profit_abs == 0]),
|
'draws': len(result[result['profit_abs'] == 0]),
|
||||||
'losses': len(result[result.profit_abs < 0]),
|
'losses': len(result[result['profit_abs'] < 0]),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -102,8 +102,8 @@ def generate_pair_metrics(data: Dict[str, Dict], stake_currency: str, max_open_t
|
|||||||
tabular_data = []
|
tabular_data = []
|
||||||
|
|
||||||
for pair in data:
|
for pair in data:
|
||||||
result = results[results.pair == pair]
|
result = results[results['pair'] == pair]
|
||||||
if skip_nan and result.profit_abs.isnull().all():
|
if skip_nan and result['profit_abs'].isnull().all():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
tabular_data.append(_generate_result_line(result, max_open_trades, pair))
|
tabular_data.append(_generate_result_line(result, max_open_trades, pair))
|
||||||
@ -113,25 +113,6 @@ def generate_pair_metrics(data: Dict[str, Dict], stake_currency: str, max_open_t
|
|||||||
return tabular_data
|
return tabular_data
|
||||||
|
|
||||||
|
|
||||||
def generate_text_table(pair_results: List[Dict[str, Any]], stake_currency: str) -> str:
|
|
||||||
"""
|
|
||||||
Generates and returns a text table for the given backtest data and the results dataframe
|
|
||||||
:param pair_results: List of Dictionaries - one entry per pair + final TOTAL row
|
|
||||||
:param stake_currency: stake-currency - used to correctly name headers
|
|
||||||
:return: pretty printed table with tabulate as string
|
|
||||||
"""
|
|
||||||
|
|
||||||
headers = _get_line_header('Pair', stake_currency)
|
|
||||||
floatfmt = _get_line_floatfmt()
|
|
||||||
output = [[
|
|
||||||
t['key'], t['trades'], t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'],
|
|
||||||
t['profit_total_pct'], t['duration_avg'], t['wins'], t['draws'], t['losses']
|
|
||||||
] for t in pair_results]
|
|
||||||
# Ignore type as floatfmt does allow tuples but mypy does not know that
|
|
||||||
return tabulate(output, headers=headers,
|
|
||||||
floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List[Dict]:
|
def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List[Dict]:
|
||||||
"""
|
"""
|
||||||
Generate small table outlining Backtest results
|
Generate small table outlining Backtest results
|
||||||
@ -166,33 +147,6 @@ def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List
|
|||||||
return tabular_data
|
return tabular_data
|
||||||
|
|
||||||
|
|
||||||
def generate_text_table_sell_reason(sell_reason_stats: List[Dict[str, Any]],
|
|
||||||
stake_currency: str) -> str:
|
|
||||||
"""
|
|
||||||
Generate small table outlining Backtest results
|
|
||||||
:param sell_reason_stats: Sell reason metrics
|
|
||||||
:param stake_currency: Stakecurrency used
|
|
||||||
:return: pretty printed table with tabulate as string
|
|
||||||
"""
|
|
||||||
headers = [
|
|
||||||
'Sell Reason',
|
|
||||||
'Sells',
|
|
||||||
'Wins',
|
|
||||||
'Draws',
|
|
||||||
'Losses',
|
|
||||||
'Avg Profit %',
|
|
||||||
'Cum Profit %',
|
|
||||||
f'Tot Profit {stake_currency}',
|
|
||||||
'Tot Profit %',
|
|
||||||
]
|
|
||||||
|
|
||||||
output = [[
|
|
||||||
t['sell_reason'], t['trades'], t['wins'], t['draws'], t['losses'],
|
|
||||||
t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'], t['profit_pct_total'],
|
|
||||||
] for t in sell_reason_stats]
|
|
||||||
return tabulate(output, headers=headers, tablefmt="orgtbl", stralign="right")
|
|
||||||
|
|
||||||
|
|
||||||
def generate_strategy_metrics(stake_currency: str, max_open_trades: int,
|
def generate_strategy_metrics(stake_currency: str, max_open_trades: int,
|
||||||
all_results: Dict) -> List[Dict]:
|
all_results: Dict) -> List[Dict]:
|
||||||
"""
|
"""
|
||||||
@ -209,26 +163,6 @@ def generate_strategy_metrics(stake_currency: str, max_open_trades: int,
|
|||||||
return tabular_data
|
return tabular_data
|
||||||
|
|
||||||
|
|
||||||
def generate_text_table_strategy(strategy_results, stake_currency: str) -> str:
|
|
||||||
"""
|
|
||||||
Generate summary table per strategy
|
|
||||||
:param stake_currency: stake-currency - used to correctly name headers
|
|
||||||
:param max_open_trades: Maximum allowed open trades used for backtest
|
|
||||||
:param all_results: Dict of <Strategyname: BacktestResult> containing results for all strategies
|
|
||||||
:return: pretty printed table with tabulate as string
|
|
||||||
"""
|
|
||||||
floatfmt = _get_line_floatfmt()
|
|
||||||
headers = _get_line_header('Strategy', stake_currency)
|
|
||||||
|
|
||||||
output = [[
|
|
||||||
t['key'], t['trades'], t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'],
|
|
||||||
t['profit_total_pct'], t['duration_avg'], t['wins'], t['draws'], t['losses']
|
|
||||||
] for t in strategy_results]
|
|
||||||
# Ignore type as floatfmt does allow tuples but mypy does not know that
|
|
||||||
return tabulate(output, headers=headers,
|
|
||||||
floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
def generate_edge_table(results: dict) -> str:
|
def generate_edge_table(results: dict) -> str:
|
||||||
|
|
||||||
floatfmt = ('s', '.10g', '.2f', '.2f', '.2f', '.2f', 'd', 'd', 'd')
|
floatfmt = ('s', '.10g', '.2f', '.2f', '.2f', '.2f', 'd', 'd', 'd')
|
||||||
@ -256,7 +190,14 @@ def generate_edge_table(results: dict) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def generate_backtest_stats(config: Dict, btdata: Dict[str, DataFrame],
|
def generate_backtest_stats(config: Dict, btdata: Dict[str, DataFrame],
|
||||||
all_results: Dict[str, DataFrame]):
|
all_results: Dict[str, DataFrame]) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
:param config: Configuration object used for backtest
|
||||||
|
:param btdata: Backtest data
|
||||||
|
:param all_results: backtest result - dictionary with { Strategy: results}.
|
||||||
|
:return:
|
||||||
|
Dictionary containing results per strategy and a stratgy summary.
|
||||||
|
"""
|
||||||
stake_currency = config['stake_currency']
|
stake_currency = config['stake_currency']
|
||||||
max_open_trades = config['max_open_trades']
|
max_open_trades = config['max_open_trades']
|
||||||
result: Dict[str, Any] = {'strategy': {}}
|
result: Dict[str, Any] = {'strategy': {}}
|
||||||
@ -288,6 +229,75 @@ def generate_backtest_stats(config: Dict, btdata: Dict[str, DataFrame],
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
###
|
||||||
|
# Start output section
|
||||||
|
###
|
||||||
|
|
||||||
|
def text_table_bt_results(pair_results: List[Dict[str, Any]], stake_currency: str) -> str:
|
||||||
|
"""
|
||||||
|
Generates and returns a text table for the given backtest data and the results dataframe
|
||||||
|
:param pair_results: List of Dictionaries - one entry per pair + final TOTAL row
|
||||||
|
:param stake_currency: stake-currency - used to correctly name headers
|
||||||
|
:return: pretty printed table with tabulate as string
|
||||||
|
"""
|
||||||
|
|
||||||
|
headers = _get_line_header('Pair', stake_currency)
|
||||||
|
floatfmt = _get_line_floatfmt()
|
||||||
|
output = [[
|
||||||
|
t['key'], t['trades'], t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'],
|
||||||
|
t['profit_total_pct'], t['duration_avg'], t['wins'], t['draws'], t['losses']
|
||||||
|
] for t in pair_results]
|
||||||
|
# Ignore type as floatfmt does allow tuples but mypy does not know that
|
||||||
|
return tabulate(output, headers=headers,
|
||||||
|
floatfmt=floatfmt, tablefmt="orgtbl", stralign="right")
|
||||||
|
|
||||||
|
|
||||||
|
def text_table_sell_reason(sell_reason_stats: List[Dict[str, Any]], stake_currency: str) -> str:
|
||||||
|
"""
|
||||||
|
Generate small table outlining Backtest results
|
||||||
|
:param sell_reason_stats: Sell reason metrics
|
||||||
|
:param stake_currency: Stakecurrency used
|
||||||
|
:return: pretty printed table with tabulate as string
|
||||||
|
"""
|
||||||
|
headers = [
|
||||||
|
'Sell Reason',
|
||||||
|
'Sells',
|
||||||
|
'Wins',
|
||||||
|
'Draws',
|
||||||
|
'Losses',
|
||||||
|
'Avg Profit %',
|
||||||
|
'Cum Profit %',
|
||||||
|
f'Tot Profit {stake_currency}',
|
||||||
|
'Tot Profit %',
|
||||||
|
]
|
||||||
|
|
||||||
|
output = [[
|
||||||
|
t['sell_reason'], t['trades'], t['wins'], t['draws'], t['losses'],
|
||||||
|
t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'], t['profit_pct_total'],
|
||||||
|
] for t in sell_reason_stats]
|
||||||
|
return tabulate(output, headers=headers, tablefmt="orgtbl", stralign="right")
|
||||||
|
|
||||||
|
|
||||||
|
def text_table_strategy(strategy_results, stake_currency: str) -> str:
|
||||||
|
"""
|
||||||
|
Generate summary table per strategy
|
||||||
|
:param stake_currency: stake-currency - used to correctly name headers
|
||||||
|
:param max_open_trades: Maximum allowed open trades used for backtest
|
||||||
|
:param all_results: Dict of <Strategyname: BacktestResult> containing results for all strategies
|
||||||
|
:return: pretty printed table with tabulate as string
|
||||||
|
"""
|
||||||
|
floatfmt = _get_line_floatfmt()
|
||||||
|
headers = _get_line_header('Strategy', stake_currency)
|
||||||
|
|
||||||
|
output = [[
|
||||||
|
t['key'], t['trades'], t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'],
|
||||||
|
t['profit_total_pct'], t['duration_avg'], t['wins'], t['draws'], t['losses']
|
||||||
|
] for t in strategy_results]
|
||||||
|
# Ignore type as floatfmt does allow tuples but mypy does not know that
|
||||||
|
return tabulate(output, headers=headers,
|
||||||
|
floatfmt=floatfmt, tablefmt="orgtbl", stralign="right")
|
||||||
|
|
||||||
|
|
||||||
def show_backtest_results(config: Dict, backtest_stats: Dict):
|
def show_backtest_results(config: Dict, backtest_stats: Dict):
|
||||||
stake_currency = config['stake_currency']
|
stake_currency = config['stake_currency']
|
||||||
|
|
||||||
@ -295,19 +305,18 @@ def show_backtest_results(config: Dict, backtest_stats: Dict):
|
|||||||
|
|
||||||
# Print results
|
# Print results
|
||||||
print(f"Result for strategy {strategy}")
|
print(f"Result for strategy {strategy}")
|
||||||
table = generate_text_table(results['results_per_pair'], stake_currency=stake_currency)
|
table = text_table_bt_results(results['results_per_pair'], stake_currency=stake_currency)
|
||||||
if isinstance(table, str):
|
if isinstance(table, str):
|
||||||
print(' BACKTESTING REPORT '.center(len(table.splitlines()[0]), '='))
|
print(' BACKTESTING REPORT '.center(len(table.splitlines()[0]), '='))
|
||||||
print(table)
|
print(table)
|
||||||
|
|
||||||
table = generate_text_table_sell_reason(sell_reason_stats=results['sell_reason_summary'],
|
table = text_table_sell_reason(sell_reason_stats=results['sell_reason_summary'],
|
||||||
stake_currency=stake_currency,
|
stake_currency=stake_currency)
|
||||||
)
|
|
||||||
if isinstance(table, str):
|
if isinstance(table, str):
|
||||||
print(' SELL REASON STATS '.center(len(table.splitlines()[0]), '='))
|
print(' SELL REASON STATS '.center(len(table.splitlines()[0]), '='))
|
||||||
print(table)
|
print(table)
|
||||||
|
|
||||||
table = generate_text_table(results['left_open_trades'], stake_currency=stake_currency)
|
table = text_table_bt_results(results['left_open_trades'], stake_currency=stake_currency)
|
||||||
if isinstance(table, str):
|
if isinstance(table, str):
|
||||||
print(' LEFT OPEN TRADES REPORT '.center(len(table.splitlines()[0]), '='))
|
print(' LEFT OPEN TRADES REPORT '.center(len(table.splitlines()[0]), '='))
|
||||||
print(table)
|
print(table)
|
||||||
@ -318,7 +327,7 @@ def show_backtest_results(config: Dict, backtest_stats: Dict):
|
|||||||
if len(backtest_stats['strategy']) > 1:
|
if len(backtest_stats['strategy']) > 1:
|
||||||
# Print Strategy summary table
|
# Print Strategy summary table
|
||||||
|
|
||||||
table = generate_text_table_strategy(backtest_stats['strategy_comparison'], stake_currency)
|
table = text_table_strategy(backtest_stats['strategy_comparison'], stake_currency)
|
||||||
print(' STRATEGY SUMMARY '.center(len(table.splitlines()[0]), '='))
|
print(' STRATEGY SUMMARY '.center(len(table.splitlines()[0]), '='))
|
||||||
print(table)
|
print(table)
|
||||||
print('=' * len(table.splitlines()[0]))
|
print('=' * len(table.splitlines()[0]))
|
||||||
|
@ -150,6 +150,9 @@ class IPairList(ABC):
|
|||||||
black_listed
|
black_listed
|
||||||
"""
|
"""
|
||||||
markets = self._exchange.markets
|
markets = self._exchange.markets
|
||||||
|
if not markets:
|
||||||
|
raise OperationalException(
|
||||||
|
'Markets not loaded. Make sure that exchange is initialized correctly.')
|
||||||
|
|
||||||
sanitized_whitelist: List[str] = []
|
sanitized_whitelist: List[str] = []
|
||||||
for pair in pairlist:
|
for pair in pairlist:
|
||||||
|
@ -380,7 +380,7 @@ class Trade(_DECL_BASE):
|
|||||||
elif order_type in ('market', 'limit') and order['side'] == 'sell':
|
elif order_type in ('market', 'limit') and order['side'] == 'sell':
|
||||||
self.close(order['price'])
|
self.close(order['price'])
|
||||||
logger.info('%s_SELL has been fulfilled for %s.', order_type.upper(), self)
|
logger.info('%s_SELL has been fulfilled for %s.', order_type.upper(), self)
|
||||||
elif order_type in ('stop_loss_limit', 'stop-loss'):
|
elif order_type in ('stop_loss_limit', 'stop-loss', 'stop'):
|
||||||
self.stoploss_order_id = None
|
self.stoploss_order_id = None
|
||||||
self.close_rate_requested = self.stop_loss
|
self.close_rate_requested = self.stop_loss
|
||||||
logger.info('%s is hit for %s.', order_type.upper(), self)
|
logger.info('%s is hit for %s.', order_type.upper(), self)
|
||||||
|
@ -162,7 +162,7 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots:
|
|||||||
# Trades can be empty
|
# Trades can be empty
|
||||||
if trades is not None and len(trades) > 0:
|
if trades is not None and len(trades) > 0:
|
||||||
# Create description for sell summarizing the trade
|
# Create description for sell summarizing the trade
|
||||||
trades['desc'] = trades.apply(lambda row: f"{round(row['profitperc'] * 100, 1)}%, "
|
trades['desc'] = trades.apply(lambda row: f"{round(row['profit_percent'] * 100, 1)}%, "
|
||||||
f"{row['sell_reason']}, {row['duration']} min",
|
f"{row['sell_reason']}, {row['duration']} min",
|
||||||
axis=1)
|
axis=1)
|
||||||
trade_buys = go.Scatter(
|
trade_buys = go.Scatter(
|
||||||
@ -181,9 +181,9 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots:
|
|||||||
)
|
)
|
||||||
|
|
||||||
trade_sells = go.Scatter(
|
trade_sells = go.Scatter(
|
||||||
x=trades.loc[trades['profitperc'] > 0, "close_time"],
|
x=trades.loc[trades['profit_percent'] > 0, "close_time"],
|
||||||
y=trades.loc[trades['profitperc'] > 0, "close_rate"],
|
y=trades.loc[trades['profit_percent'] > 0, "close_rate"],
|
||||||
text=trades.loc[trades['profitperc'] > 0, "desc"],
|
text=trades.loc[trades['profit_percent'] > 0, "desc"],
|
||||||
mode='markers',
|
mode='markers',
|
||||||
name='Sell - Profit',
|
name='Sell - Profit',
|
||||||
marker=dict(
|
marker=dict(
|
||||||
@ -194,9 +194,9 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
trade_sells_loss = go.Scatter(
|
trade_sells_loss = go.Scatter(
|
||||||
x=trades.loc[trades['profitperc'] <= 0, "close_time"],
|
x=trades.loc[trades['profit_percent'] <= 0, "close_time"],
|
||||||
y=trades.loc[trades['profitperc'] <= 0, "close_rate"],
|
y=trades.loc[trades['profit_percent'] <= 0, "close_rate"],
|
||||||
text=trades.loc[trades['profitperc'] <= 0, "desc"],
|
text=trades.loc[trades['profit_percent'] <= 0, "desc"],
|
||||||
mode='markers',
|
mode='markers',
|
||||||
name='Sell - Loss',
|
name='Sell - Loss',
|
||||||
marker=dict(
|
marker=dict(
|
||||||
|
@ -172,8 +172,8 @@ class ApiServer(RPC):
|
|||||||
self.app.add_url_rule(f'{BASE_URI}/stop', 'stop', view_func=self._stop, methods=['POST'])
|
self.app.add_url_rule(f'{BASE_URI}/stop', 'stop', view_func=self._stop, methods=['POST'])
|
||||||
self.app.add_url_rule(f'{BASE_URI}/stopbuy', 'stopbuy',
|
self.app.add_url_rule(f'{BASE_URI}/stopbuy', 'stopbuy',
|
||||||
view_func=self._stopbuy, methods=['POST'])
|
view_func=self._stopbuy, methods=['POST'])
|
||||||
self.app.add_url_rule(f'{BASE_URI}/reload_conf', 'reload_conf',
|
self.app.add_url_rule(f'{BASE_URI}/reload_config', 'reload_config',
|
||||||
view_func=self._reload_conf, methods=['POST'])
|
view_func=self._reload_config, methods=['POST'])
|
||||||
# Info commands
|
# Info commands
|
||||||
self.app.add_url_rule(f'{BASE_URI}/balance', 'balance',
|
self.app.add_url_rule(f'{BASE_URI}/balance', 'balance',
|
||||||
view_func=self._balance, methods=['GET'])
|
view_func=self._balance, methods=['GET'])
|
||||||
@ -304,12 +304,12 @@ class ApiServer(RPC):
|
|||||||
|
|
||||||
@require_login
|
@require_login
|
||||||
@rpc_catch_errors
|
@rpc_catch_errors
|
||||||
def _reload_conf(self):
|
def _reload_config(self):
|
||||||
"""
|
"""
|
||||||
Handler for /reload_conf.
|
Handler for /reload_config.
|
||||||
Triggers a config file reload
|
Triggers a config file reload
|
||||||
"""
|
"""
|
||||||
msg = self._rpc_reload_conf()
|
msg = self._rpc_reload_config()
|
||||||
return self.rest_dump(msg)
|
return self.rest_dump(msg)
|
||||||
|
|
||||||
@require_login
|
@require_login
|
||||||
|
@ -106,6 +106,8 @@ class RPC:
|
|||||||
'exchange': config['exchange']['name'],
|
'exchange': config['exchange']['name'],
|
||||||
'strategy': config['strategy'],
|
'strategy': config['strategy'],
|
||||||
'forcebuy_enabled': config.get('forcebuy_enable', False),
|
'forcebuy_enabled': config.get('forcebuy_enable', False),
|
||||||
|
'ask_strategy': config.get('ask_strategy', {}),
|
||||||
|
'bid_strategy': config.get('bid_strategy', {}),
|
||||||
'state': str(self._freqtrade.state)
|
'state': str(self._freqtrade.state)
|
||||||
}
|
}
|
||||||
return val
|
return val
|
||||||
@ -131,6 +133,14 @@ class RPC:
|
|||||||
except DependencyException:
|
except DependencyException:
|
||||||
current_rate = NAN
|
current_rate = NAN
|
||||||
current_profit = trade.calc_profit_ratio(current_rate)
|
current_profit = trade.calc_profit_ratio(current_rate)
|
||||||
|
current_profit_abs = trade.calc_profit(current_rate)
|
||||||
|
# Calculate guaranteed profit (in case of trailing stop)
|
||||||
|
stoploss_entry_dist = trade.calc_profit(trade.stop_loss)
|
||||||
|
stoploss_entry_dist_ratio = trade.calc_profit_ratio(trade.stop_loss)
|
||||||
|
# calculate distance to stoploss
|
||||||
|
stoploss_current_dist = trade.stop_loss - current_rate
|
||||||
|
stoploss_current_dist_ratio = stoploss_current_dist / current_rate
|
||||||
|
|
||||||
fmt_close_profit = (f'{round(trade.close_profit * 100, 2):.2f}%'
|
fmt_close_profit = (f'{round(trade.close_profit * 100, 2):.2f}%'
|
||||||
if trade.close_profit is not None else None)
|
if trade.close_profit is not None else None)
|
||||||
trade_dict = trade.to_json()
|
trade_dict = trade.to_json()
|
||||||
@ -141,6 +151,11 @@ class RPC:
|
|||||||
current_rate=current_rate,
|
current_rate=current_rate,
|
||||||
current_profit=current_profit,
|
current_profit=current_profit,
|
||||||
current_profit_pct=round(current_profit * 100, 2),
|
current_profit_pct=round(current_profit * 100, 2),
|
||||||
|
current_profit_abs=current_profit_abs,
|
||||||
|
stoploss_current_dist=stoploss_current_dist,
|
||||||
|
stoploss_current_dist_ratio=round(stoploss_current_dist_ratio, 8),
|
||||||
|
stoploss_entry_dist=stoploss_entry_dist,
|
||||||
|
stoploss_entry_dist_ratio=round(stoploss_entry_dist_ratio, 8),
|
||||||
open_order='({} {} rem={:.8f})'.format(
|
open_order='({} {} rem={:.8f})'.format(
|
||||||
order['type'], order['side'], order['remaining']
|
order['type'], order['side'], order['remaining']
|
||||||
) if order else None,
|
) if order else None,
|
||||||
@ -284,8 +299,9 @@ class RPC:
|
|||||||
|
|
||||||
# Prepare data to display
|
# Prepare data to display
|
||||||
profit_closed_coin_sum = round(sum(profit_closed_coin), 8)
|
profit_closed_coin_sum = round(sum(profit_closed_coin), 8)
|
||||||
profit_closed_percent = (round(mean(profit_closed_ratio) * 100, 2) if profit_closed_ratio
|
profit_closed_ratio_mean = mean(profit_closed_ratio) if profit_closed_ratio else 0.0
|
||||||
else 0.0)
|
profit_closed_ratio_sum = sum(profit_closed_ratio) if profit_closed_ratio else 0.0
|
||||||
|
|
||||||
profit_closed_fiat = self._fiat_converter.convert_amount(
|
profit_closed_fiat = self._fiat_converter.convert_amount(
|
||||||
profit_closed_coin_sum,
|
profit_closed_coin_sum,
|
||||||
stake_currency,
|
stake_currency,
|
||||||
@ -293,7 +309,8 @@ class RPC:
|
|||||||
) if self._fiat_converter else 0
|
) if self._fiat_converter else 0
|
||||||
|
|
||||||
profit_all_coin_sum = round(sum(profit_all_coin), 8)
|
profit_all_coin_sum = round(sum(profit_all_coin), 8)
|
||||||
profit_all_percent = round(mean(profit_all_ratio) * 100, 2) if profit_all_ratio else 0.0
|
profit_all_ratio_mean = mean(profit_all_ratio) if profit_all_ratio else 0.0
|
||||||
|
profit_all_ratio_sum = sum(profit_all_ratio) if profit_all_ratio else 0.0
|
||||||
profit_all_fiat = self._fiat_converter.convert_amount(
|
profit_all_fiat = self._fiat_converter.convert_amount(
|
||||||
profit_all_coin_sum,
|
profit_all_coin_sum,
|
||||||
stake_currency,
|
stake_currency,
|
||||||
@ -305,10 +322,18 @@ class RPC:
|
|||||||
num = float(len(durations) or 1)
|
num = float(len(durations) or 1)
|
||||||
return {
|
return {
|
||||||
'profit_closed_coin': profit_closed_coin_sum,
|
'profit_closed_coin': profit_closed_coin_sum,
|
||||||
'profit_closed_percent': profit_closed_percent,
|
'profit_closed_percent': round(profit_closed_ratio_mean * 100, 2), # DEPRECATED
|
||||||
|
'profit_closed_percent_mean': round(profit_closed_ratio_mean * 100, 2),
|
||||||
|
'profit_closed_ratio_mean': profit_closed_ratio_mean,
|
||||||
|
'profit_closed_percent_sum': round(profit_closed_ratio_sum * 100, 2),
|
||||||
|
'profit_closed_ratio_sum': profit_closed_ratio_sum,
|
||||||
'profit_closed_fiat': profit_closed_fiat,
|
'profit_closed_fiat': profit_closed_fiat,
|
||||||
'profit_all_coin': profit_all_coin_sum,
|
'profit_all_coin': profit_all_coin_sum,
|
||||||
'profit_all_percent': profit_all_percent,
|
'profit_all_percent': round(profit_all_ratio_mean * 100, 2), # DEPRECATED
|
||||||
|
'profit_all_percent_mean': round(profit_all_ratio_mean * 100, 2),
|
||||||
|
'profit_all_ratio_mean': profit_all_ratio_mean,
|
||||||
|
'profit_all_percent_sum': round(profit_all_ratio_sum * 100, 2),
|
||||||
|
'profit_all_ratio_sum': profit_all_ratio_sum,
|
||||||
'profit_all_fiat': profit_all_fiat,
|
'profit_all_fiat': profit_all_fiat,
|
||||||
'trade_count': len(trades),
|
'trade_count': len(trades),
|
||||||
'closed_trade_count': len([t for t in trades if not t.is_open]),
|
'closed_trade_count': len([t for t in trades if not t.is_open]),
|
||||||
@ -394,9 +419,9 @@ class RPC:
|
|||||||
|
|
||||||
return {'status': 'already stopped'}
|
return {'status': 'already stopped'}
|
||||||
|
|
||||||
def _rpc_reload_conf(self) -> Dict[str, str]:
|
def _rpc_reload_config(self) -> Dict[str, str]:
|
||||||
""" Handler for reload_conf. """
|
""" Handler for reload_config. """
|
||||||
self._freqtrade.state = State.RELOAD_CONF
|
self._freqtrade.state = State.RELOAD_CONFIG
|
||||||
return {'status': 'reloading config ...'}
|
return {'status': 'reloading config ...'}
|
||||||
|
|
||||||
def _rpc_stopbuy(self) -> Dict[str, str]:
|
def _rpc_stopbuy(self) -> Dict[str, str]:
|
||||||
@ -407,7 +432,7 @@ class RPC:
|
|||||||
# Set 'max_open_trades' to 0
|
# Set 'max_open_trades' to 0
|
||||||
self._freqtrade.config['max_open_trades'] = 0
|
self._freqtrade.config['max_open_trades'] = 0
|
||||||
|
|
||||||
return {'status': 'No more buy will occur from now. Run /reload_conf to reset.'}
|
return {'status': 'No more buy will occur from now. Run /reload_config to reset.'}
|
||||||
|
|
||||||
def _rpc_forcesell(self, trade_id: str) -> Dict[str, str]:
|
def _rpc_forcesell(self, trade_id: str) -> Dict[str, str]:
|
||||||
"""
|
"""
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
"""
|
"""
|
||||||
This module manage Telegram communication
|
This module manage Telegram communication
|
||||||
"""
|
"""
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Callable, Dict
|
from typing import Any, Callable, Dict
|
||||||
|
|
||||||
@ -19,7 +20,6 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
logger.debug('Included module rpc.telegram ...')
|
logger.debug('Included module rpc.telegram ...')
|
||||||
|
|
||||||
|
|
||||||
MAX_TELEGRAM_MESSAGE_LENGTH = 4096
|
MAX_TELEGRAM_MESSAGE_LENGTH = 4096
|
||||||
|
|
||||||
|
|
||||||
@ -29,6 +29,7 @@ def authorized_only(command_handler: Callable[..., None]) -> Callable[..., Any]:
|
|||||||
:param command_handler: Telegram CommandHandler
|
:param command_handler: Telegram CommandHandler
|
||||||
:return: decorated function
|
:return: decorated function
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def wrapper(self, *args, **kwargs):
|
def wrapper(self, *args, **kwargs):
|
||||||
""" Decorator logic """
|
""" Decorator logic """
|
||||||
update = kwargs.get('update') or args[0]
|
update = kwargs.get('update') or args[0]
|
||||||
@ -94,8 +95,8 @@ class Telegram(RPC):
|
|||||||
CommandHandler('performance', self._performance),
|
CommandHandler('performance', self._performance),
|
||||||
CommandHandler('daily', self._daily),
|
CommandHandler('daily', self._daily),
|
||||||
CommandHandler('count', self._count),
|
CommandHandler('count', self._count),
|
||||||
CommandHandler('reload_conf', self._reload_conf),
|
CommandHandler(['reload_config', 'reload_conf'], self._reload_config),
|
||||||
CommandHandler('show_config', self._show_config),
|
CommandHandler(['show_config', 'show_conf'], self._show_config),
|
||||||
CommandHandler('stopbuy', self._stopbuy),
|
CommandHandler('stopbuy', self._stopbuy),
|
||||||
CommandHandler('whitelist', self._whitelist),
|
CommandHandler('whitelist', self._whitelist),
|
||||||
CommandHandler('blacklist', self._blacklist),
|
CommandHandler('blacklist', self._blacklist),
|
||||||
@ -133,7 +134,7 @@ class Telegram(RPC):
|
|||||||
else:
|
else:
|
||||||
msg['stake_amount_fiat'] = 0
|
msg['stake_amount_fiat'] = 0
|
||||||
|
|
||||||
message = ("*{exchange}:* Buying {pair}\n"
|
message = ("\N{LARGE BLUE CIRCLE} *{exchange}:* Buying {pair}\n"
|
||||||
"*Amount:* `{amount:.8f}`\n"
|
"*Amount:* `{amount:.8f}`\n"
|
||||||
"*Open Rate:* `{limit:.8f}`\n"
|
"*Open Rate:* `{limit:.8f}`\n"
|
||||||
"*Current Rate:* `{current_rate:.8f}`\n"
|
"*Current Rate:* `{current_rate:.8f}`\n"
|
||||||
@ -144,7 +145,8 @@ class Telegram(RPC):
|
|||||||
message += ")`"
|
message += ")`"
|
||||||
|
|
||||||
elif msg['type'] == RPCMessageType.BUY_CANCEL_NOTIFICATION:
|
elif msg['type'] == RPCMessageType.BUY_CANCEL_NOTIFICATION:
|
||||||
message = "*{exchange}:* Cancelling Open Buy Order for {pair}".format(**msg)
|
message = ("\N{WARNING SIGN} *{exchange}:* "
|
||||||
|
"Cancelling Open Buy Order for {pair}".format(**msg))
|
||||||
|
|
||||||
elif msg['type'] == RPCMessageType.SELL_NOTIFICATION:
|
elif msg['type'] == RPCMessageType.SELL_NOTIFICATION:
|
||||||
msg['amount'] = round(msg['amount'], 8)
|
msg['amount'] = round(msg['amount'], 8)
|
||||||
@ -153,7 +155,9 @@ class Telegram(RPC):
|
|||||||
microsecond=0) - msg['open_date'].replace(microsecond=0)
|
microsecond=0) - msg['open_date'].replace(microsecond=0)
|
||||||
msg['duration_min'] = msg['duration'].total_seconds() / 60
|
msg['duration_min'] = msg['duration'].total_seconds() / 60
|
||||||
|
|
||||||
message = ("*{exchange}:* Selling {pair}\n"
|
msg['emoji'] = self._get_sell_emoji(msg)
|
||||||
|
|
||||||
|
message = ("{emoji} *{exchange}:* Selling {pair}\n"
|
||||||
"*Amount:* `{amount:.8f}`\n"
|
"*Amount:* `{amount:.8f}`\n"
|
||||||
"*Open Rate:* `{open_rate:.8f}`\n"
|
"*Open Rate:* `{open_rate:.8f}`\n"
|
||||||
"*Current Rate:* `{current_rate:.8f}`\n"
|
"*Current Rate:* `{current_rate:.8f}`\n"
|
||||||
@ -172,14 +176,14 @@ class Telegram(RPC):
|
|||||||
' / {profit_fiat:.3f} {fiat_currency})`').format(**msg)
|
' / {profit_fiat:.3f} {fiat_currency})`').format(**msg)
|
||||||
|
|
||||||
elif msg['type'] == RPCMessageType.SELL_CANCEL_NOTIFICATION:
|
elif msg['type'] == RPCMessageType.SELL_CANCEL_NOTIFICATION:
|
||||||
message = ("*{exchange}:* Cancelling Open Sell Order "
|
message = ("\N{WARNING SIGN} *{exchange}:* Cancelling Open Sell Order "
|
||||||
"for {pair}. Reason: {reason}").format(**msg)
|
"for {pair}. Reason: {reason}").format(**msg)
|
||||||
|
|
||||||
elif msg['type'] == RPCMessageType.STATUS_NOTIFICATION:
|
elif msg['type'] == RPCMessageType.STATUS_NOTIFICATION:
|
||||||
message = '*Status:* `{status}`'.format(**msg)
|
message = '*Status:* `{status}`'.format(**msg)
|
||||||
|
|
||||||
elif msg['type'] == RPCMessageType.WARNING_NOTIFICATION:
|
elif msg['type'] == RPCMessageType.WARNING_NOTIFICATION:
|
||||||
message = '*Warning:* `{status}`'.format(**msg)
|
message = '\N{WARNING SIGN} *Warning:* `{status}`'.format(**msg)
|
||||||
|
|
||||||
elif msg['type'] == RPCMessageType.CUSTOM_NOTIFICATION:
|
elif msg['type'] == RPCMessageType.CUSTOM_NOTIFICATION:
|
||||||
message = '{status}'.format(**msg)
|
message = '{status}'.format(**msg)
|
||||||
@ -189,6 +193,20 @@ class Telegram(RPC):
|
|||||||
|
|
||||||
self._send_msg(message)
|
self._send_msg(message)
|
||||||
|
|
||||||
|
def _get_sell_emoji(self, msg):
|
||||||
|
"""
|
||||||
|
Get emoji for sell-side
|
||||||
|
"""
|
||||||
|
|
||||||
|
if float(msg['profit_percent']) >= 5.0:
|
||||||
|
return "\N{ROCKET}"
|
||||||
|
elif float(msg['profit_percent']) >= 0.0:
|
||||||
|
return "\N{EIGHT SPOKED ASTERISK}"
|
||||||
|
elif msg['sell_reason'] == "stop_loss":
|
||||||
|
return"\N{WARNING SIGN}"
|
||||||
|
else:
|
||||||
|
return "\N{CROSS MARK}"
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _status(self, update: Update, context: CallbackContext) -> None:
|
def _status(self, update: Update, context: CallbackContext) -> None:
|
||||||
"""
|
"""
|
||||||
@ -315,10 +333,12 @@ class Telegram(RPC):
|
|||||||
stake_cur,
|
stake_cur,
|
||||||
fiat_disp_cur)
|
fiat_disp_cur)
|
||||||
profit_closed_coin = stats['profit_closed_coin']
|
profit_closed_coin = stats['profit_closed_coin']
|
||||||
profit_closed_percent = stats['profit_closed_percent']
|
profit_closed_percent_mean = stats['profit_closed_percent_mean']
|
||||||
|
profit_closed_percent_sum = stats['profit_closed_percent_sum']
|
||||||
profit_closed_fiat = stats['profit_closed_fiat']
|
profit_closed_fiat = stats['profit_closed_fiat']
|
||||||
profit_all_coin = stats['profit_all_coin']
|
profit_all_coin = stats['profit_all_coin']
|
||||||
profit_all_percent = stats['profit_all_percent']
|
profit_all_percent_mean = stats['profit_all_percent_mean']
|
||||||
|
profit_all_percent_sum = stats['profit_all_percent_sum']
|
||||||
profit_all_fiat = stats['profit_all_fiat']
|
profit_all_fiat = stats['profit_all_fiat']
|
||||||
trade_count = stats['trade_count']
|
trade_count = stats['trade_count']
|
||||||
first_trade_date = stats['first_trade_date']
|
first_trade_date = stats['first_trade_date']
|
||||||
@ -333,13 +353,16 @@ class Telegram(RPC):
|
|||||||
if stats['closed_trade_count'] > 0:
|
if stats['closed_trade_count'] > 0:
|
||||||
markdown_msg = ("*ROI:* Closed trades\n"
|
markdown_msg = ("*ROI:* Closed trades\n"
|
||||||
f"∙ `{profit_closed_coin:.8f} {stake_cur} "
|
f"∙ `{profit_closed_coin:.8f} {stake_cur} "
|
||||||
f"({profit_closed_percent:.2f}%)`\n"
|
f"({profit_closed_percent_mean:.2f}%) "
|
||||||
|
f"({profit_closed_percent_sum} \N{GREEK CAPITAL LETTER SIGMA}%)`\n"
|
||||||
f"∙ `{profit_closed_fiat:.3f} {fiat_disp_cur}`\n")
|
f"∙ `{profit_closed_fiat:.3f} {fiat_disp_cur}`\n")
|
||||||
else:
|
else:
|
||||||
markdown_msg = "`No closed trade` \n"
|
markdown_msg = "`No closed trade` \n"
|
||||||
|
|
||||||
markdown_msg += (f"*ROI:* All trades\n"
|
markdown_msg += (f"*ROI:* All trades\n"
|
||||||
f"∙ `{profit_all_coin:.8f} {stake_cur} ({profit_all_percent:.2f}%)`\n"
|
f"∙ `{profit_all_coin:.8f} {stake_cur} "
|
||||||
|
f"({profit_all_percent_mean:.2f}%) "
|
||||||
|
f"({profit_all_percent_sum} \N{GREEK CAPITAL LETTER SIGMA}%)`\n"
|
||||||
f"∙ `{profit_all_fiat:.3f} {fiat_disp_cur}`\n"
|
f"∙ `{profit_all_fiat:.3f} {fiat_disp_cur}`\n"
|
||||||
f"*Total Trade Count:* `{trade_count}`\n"
|
f"*Total Trade Count:* `{trade_count}`\n"
|
||||||
f"*First Trade opened:* `{first_trade_date}`\n"
|
f"*First Trade opened:* `{first_trade_date}`\n"
|
||||||
@ -366,11 +389,11 @@ class Telegram(RPC):
|
|||||||
)
|
)
|
||||||
for currency in result['currencies']:
|
for currency in result['currencies']:
|
||||||
if currency['est_stake'] > 0.0001:
|
if currency['est_stake'] > 0.0001:
|
||||||
curr_output = "*{currency}:*\n" \
|
curr_output = ("*{currency}:*\n"
|
||||||
"\t`Available: {free: .8f}`\n" \
|
"\t`Available: {free: .8f}`\n"
|
||||||
"\t`Balance: {balance: .8f}`\n" \
|
"\t`Balance: {balance: .8f}`\n"
|
||||||
"\t`Pending: {used: .8f}`\n" \
|
"\t`Pending: {used: .8f}`\n"
|
||||||
"\t`Est. {stake}: {est_stake: .8f}`\n".format(**currency)
|
"\t`Est. {stake}: {est_stake: .8f}`\n").format(**currency)
|
||||||
else:
|
else:
|
||||||
curr_output = "*{currency}:* not showing <1$ amount \n".format(**currency)
|
curr_output = "*{currency}:* not showing <1$ amount \n".format(**currency)
|
||||||
|
|
||||||
@ -381,9 +404,9 @@ class Telegram(RPC):
|
|||||||
else:
|
else:
|
||||||
output += curr_output
|
output += curr_output
|
||||||
|
|
||||||
output += "\n*Estimated Value*:\n" \
|
output += ("\n*Estimated Value*:\n"
|
||||||
"\t`{stake}: {total: .8f}`\n" \
|
"\t`{stake}: {total: .8f}`\n"
|
||||||
"\t`{symbol}: {value: .2f}`\n".format(**result)
|
"\t`{symbol}: {value: .2f}`\n").format(**result)
|
||||||
self._send_msg(output)
|
self._send_msg(output)
|
||||||
except RPCException as e:
|
except RPCException as e:
|
||||||
self._send_msg(str(e))
|
self._send_msg(str(e))
|
||||||
@ -413,15 +436,15 @@ class Telegram(RPC):
|
|||||||
self._send_msg('Status: `{status}`'.format(**msg))
|
self._send_msg('Status: `{status}`'.format(**msg))
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _reload_conf(self, update: Update, context: CallbackContext) -> None:
|
def _reload_config(self, update: Update, context: CallbackContext) -> None:
|
||||||
"""
|
"""
|
||||||
Handler for /reload_conf.
|
Handler for /reload_config.
|
||||||
Triggers a config file reload
|
Triggers a config file reload
|
||||||
:param bot: telegram bot
|
:param bot: telegram bot
|
||||||
:param update: message update
|
:param update: message update
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
msg = self._rpc_reload_conf()
|
msg = self._rpc_reload_config()
|
||||||
self._send_msg('Status: `{status}`'.format(**msg))
|
self._send_msg('Status: `{status}`'.format(**msg))
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
@ -576,32 +599,32 @@ class Telegram(RPC):
|
|||||||
:param update: message update
|
:param update: message update
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
forcebuy_text = "*/forcebuy <pair> [<rate>]:* `Instantly buys the given pair. " \
|
forcebuy_text = ("*/forcebuy <pair> [<rate>]:* `Instantly buys the given pair. "
|
||||||
"Optionally takes a rate at which to buy.` \n"
|
"Optionally takes a rate at which to buy.` \n")
|
||||||
message = "*/start:* `Starts the trader`\n" \
|
message = ("*/start:* `Starts the trader`\n"
|
||||||
"*/stop:* `Stops the trader`\n" \
|
"*/stop:* `Stops the trader`\n"
|
||||||
"*/status [table]:* `Lists all open trades`\n" \
|
"*/status [table]:* `Lists all open trades`\n"
|
||||||
" *table :* `will display trades in a table`\n" \
|
" *table :* `will display trades in a table`\n"
|
||||||
" `pending buy orders are marked with an asterisk (*)`\n" \
|
" `pending buy orders are marked with an asterisk (*)`\n"
|
||||||
" `pending sell orders are marked with a double asterisk (**)`\n" \
|
" `pending sell orders are marked with a double asterisk (**)`\n"
|
||||||
"*/profit:* `Lists cumulative profit from all finished trades`\n" \
|
"*/profit:* `Lists cumulative profit from all finished trades`\n"
|
||||||
"*/forcesell <trade_id>|all:* `Instantly sells the given trade or all trades, " \
|
"*/forcesell <trade_id>|all:* `Instantly sells the given trade or all trades, "
|
||||||
"regardless of profit`\n" \
|
"regardless of profit`\n"
|
||||||
f"{forcebuy_text if self._config.get('forcebuy_enable', False) else '' }" \
|
f"{forcebuy_text if self._config.get('forcebuy_enable', False) else ''}"
|
||||||
"*/performance:* `Show performance of each finished trade grouped by pair`\n" \
|
"*/performance:* `Show performance of each finished trade grouped by pair`\n"
|
||||||
"*/daily <n>:* `Shows profit or loss per day, over the last n days`\n" \
|
"*/daily <n>:* `Shows profit or loss per day, over the last n days`\n"
|
||||||
"*/count:* `Show number of trades running compared to allowed number of trades`" \
|
"*/count:* `Show number of trades running compared to allowed number of trades`"
|
||||||
"\n" \
|
"\n"
|
||||||
"*/balance:* `Show account balance per currency`\n" \
|
"*/balance:* `Show account balance per currency`\n"
|
||||||
"*/stopbuy:* `Stops buying, but handles open trades gracefully` \n" \
|
"*/stopbuy:* `Stops buying, but handles open trades gracefully` \n"
|
||||||
"*/reload_conf:* `Reload configuration file` \n" \
|
"*/reload_config:* `Reload configuration file` \n"
|
||||||
"*/show_config:* `Show running configuration` \n" \
|
"*/show_config:* `Show running configuration` \n"
|
||||||
"*/whitelist:* `Show current whitelist` \n" \
|
"*/whitelist:* `Show current whitelist` \n"
|
||||||
"*/blacklist [pair]:* `Show current blacklist, or adds one or more pairs " \
|
"*/blacklist [pair]:* `Show current blacklist, or adds one or more pairs "
|
||||||
"to the blacklist.` \n" \
|
"to the blacklist.` \n"
|
||||||
"*/edge:* `Shows validated pairs by Edge if it is enabled` \n" \
|
"*/edge:* `Shows validated pairs by Edge if it is enabled` \n"
|
||||||
"*/help:* `This help message`\n" \
|
"*/help:* `This help message`\n"
|
||||||
"*/version:* `Show version`"
|
"*/version:* `Show version`")
|
||||||
|
|
||||||
self._send_msg(message)
|
self._send_msg(message)
|
||||||
|
|
||||||
@ -643,6 +666,8 @@ class Telegram(RPC):
|
|||||||
f"*Stake per trade:* `{val['stake_amount']} {val['stake_currency']}`\n"
|
f"*Stake per trade:* `{val['stake_amount']} {val['stake_currency']}`\n"
|
||||||
f"*Max open Trades:* `{val['max_open_trades']}`\n"
|
f"*Max open Trades:* `{val['max_open_trades']}`\n"
|
||||||
f"*Minimum ROI:* `{val['minimal_roi']}`\n"
|
f"*Minimum ROI:* `{val['minimal_roi']}`\n"
|
||||||
|
f"*Ask strategy:* ```\n{json.dumps(val['ask_strategy'])}```\n"
|
||||||
|
f"*Bid strategy:* ```\n{json.dumps(val['bid_strategy'])}```\n"
|
||||||
f"{sl_info}"
|
f"{sl_info}"
|
||||||
f"*Timeframe:* `{val['timeframe']}`\n"
|
f"*Timeframe:* `{val['timeframe']}`\n"
|
||||||
f"*Strategy:* `{val['strategy']}`\n"
|
f"*Strategy:* `{val['strategy']}`\n"
|
||||||
|
@ -12,7 +12,7 @@ class State(Enum):
|
|||||||
"""
|
"""
|
||||||
RUNNING = 1
|
RUNNING = 1
|
||||||
STOPPED = 2
|
STOPPED = 2
|
||||||
RELOAD_CONF = 3
|
RELOAD_CONFIG = 3
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.name.lower()}"
|
return f"{self.name.lower()}"
|
||||||
|
@ -71,7 +71,7 @@ class Worker:
|
|||||||
state = None
|
state = None
|
||||||
while True:
|
while True:
|
||||||
state = self._worker(old_state=state)
|
state = self._worker(old_state=state)
|
||||||
if state == State.RELOAD_CONF:
|
if state == State.RELOAD_CONFIG:
|
||||||
self._reconfigure()
|
self._reconfigure()
|
||||||
|
|
||||||
def _worker(self, old_state: Optional[State]) -> State:
|
def _worker(self, old_state: Optional[State]) -> State:
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# requirements without requirements installable via conda
|
# requirements without requirements installable via conda
|
||||||
# mainly used for Raspberry pi installs
|
# mainly used for Raspberry pi installs
|
||||||
ccxt==1.29.5
|
ccxt==1.29.52
|
||||||
SQLAlchemy==1.3.17
|
SQLAlchemy==1.3.17
|
||||||
python-telegram-bot==12.7
|
python-telegram-bot==12.7
|
||||||
arrow==0.15.6
|
arrow==0.15.6
|
||||||
|
@ -7,11 +7,11 @@ coveralls==2.0.0
|
|||||||
flake8==3.8.2
|
flake8==3.8.2
|
||||||
flake8-type-annotations==0.1.0
|
flake8-type-annotations==0.1.0
|
||||||
flake8-tidy-imports==4.1.0
|
flake8-tidy-imports==4.1.0
|
||||||
mypy==0.770
|
mypy==0.780
|
||||||
pytest==5.4.2
|
pytest==5.4.3
|
||||||
pytest-asyncio==0.12.0
|
pytest-asyncio==0.12.0
|
||||||
pytest-cov==2.9.0
|
pytest-cov==2.9.0
|
||||||
pytest-mock==3.1.0
|
pytest-mock==3.1.1
|
||||||
pytest-random-order==1.0.4
|
pytest-random-order==1.0.4
|
||||||
|
|
||||||
# Convert jupyter notebooks to markdown documents
|
# Convert jupyter notebooks to markdown documents
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# Load common requirements
|
# Load common requirements
|
||||||
-r requirements-common.txt
|
-r requirements-common.txt
|
||||||
|
|
||||||
numpy==1.18.4
|
numpy==1.18.5
|
||||||
pandas==1.0.4
|
pandas==1.0.4
|
||||||
|
@ -80,18 +80,18 @@ class FtRestClient():
|
|||||||
return self._post("stop")
|
return self._post("stop")
|
||||||
|
|
||||||
def stopbuy(self):
|
def stopbuy(self):
|
||||||
"""Stop buying (but handle sells gracefully). Use `reload_conf` to reset.
|
"""Stop buying (but handle sells gracefully). Use `reload_config` to reset.
|
||||||
|
|
||||||
:return: json object
|
:return: json object
|
||||||
"""
|
"""
|
||||||
return self._post("stopbuy")
|
return self._post("stopbuy")
|
||||||
|
|
||||||
def reload_conf(self):
|
def reload_config(self):
|
||||||
"""Reload configuration.
|
"""Reload configuration.
|
||||||
|
|
||||||
:return: json object
|
:return: json object
|
||||||
"""
|
"""
|
||||||
return self._post("reload_conf")
|
return self._post("reload_config")
|
||||||
|
|
||||||
def balance(self):
|
def balance(self):
|
||||||
"""Get the account balance.
|
"""Get the account balance.
|
||||||
|
2
setup.py
2
setup.py
@ -63,7 +63,7 @@ setup(name='freqtrade',
|
|||||||
tests_require=['pytest', 'pytest-asyncio', 'pytest-cov', 'pytest-mock', ],
|
tests_require=['pytest', 'pytest-asyncio', 'pytest-cov', 'pytest-mock', ],
|
||||||
install_requires=[
|
install_requires=[
|
||||||
# from requirements-common.txt
|
# from requirements-common.txt
|
||||||
'ccxt>=1.18.1080',
|
'ccxt>=1.24.96',
|
||||||
'SQLAlchemy',
|
'SQLAlchemy',
|
||||||
'python-telegram-bot',
|
'python-telegram-bot',
|
||||||
'arrow',
|
'arrow',
|
||||||
|
@ -1590,6 +1590,7 @@ def buy_order_fee():
|
|||||||
'datetime': str(arrow.utcnow().shift(minutes=-601).datetime),
|
'datetime': str(arrow.utcnow().shift(minutes=-601).datetime),
|
||||||
'price': 0.245441,
|
'price': 0.245441,
|
||||||
'amount': 8.0,
|
'amount': 8.0,
|
||||||
|
'cost': 1.963528,
|
||||||
'remaining': 90.99181073,
|
'remaining': 90.99181073,
|
||||||
'status': 'closed',
|
'status': 'closed',
|
||||||
'fee': None
|
'fee': None
|
||||||
|
@ -47,7 +47,7 @@ def test_load_trades_from_db(default_conf, fee, mocker):
|
|||||||
assert isinstance(trades, DataFrame)
|
assert isinstance(trades, DataFrame)
|
||||||
assert "pair" in trades.columns
|
assert "pair" in trades.columns
|
||||||
assert "open_time" in trades.columns
|
assert "open_time" in trades.columns
|
||||||
assert "profitperc" in trades.columns
|
assert "profit_percent" in trades.columns
|
||||||
|
|
||||||
for col in BT_DATA_COLUMNS:
|
for col in BT_DATA_COLUMNS:
|
||||||
if col not in ['index', 'open_at_end']:
|
if col not in ['index', 'open_at_end']:
|
||||||
|
@ -25,7 +25,7 @@ from freqtrade.resolvers.exchange_resolver import ExchangeResolver
|
|||||||
from tests.conftest import get_patched_exchange, log_has, log_has_re
|
from tests.conftest import get_patched_exchange, log_has, log_has_re
|
||||||
|
|
||||||
# Make sure to always keep one exchange here which is NOT subclassed!!
|
# Make sure to always keep one exchange here which is NOT subclassed!!
|
||||||
EXCHANGES = ['bittrex', 'binance', 'kraken', ]
|
EXCHANGES = ['bittrex', 'binance', 'kraken', 'ftx']
|
||||||
|
|
||||||
|
|
||||||
# Source: https://stackoverflow.com/questions/29881236/how-to-mock-asyncio-coroutines
|
# Source: https://stackoverflow.com/questions/29881236/how-to-mock-asyncio-coroutines
|
||||||
@ -352,7 +352,7 @@ def test__load_markets(default_conf, mocker, caplog):
|
|||||||
assert ex.markets == expected_return
|
assert ex.markets == expected_return
|
||||||
|
|
||||||
|
|
||||||
def test__reload_markets(default_conf, mocker, caplog):
|
def test_reload_markets(default_conf, mocker, caplog):
|
||||||
caplog.set_level(logging.DEBUG)
|
caplog.set_level(logging.DEBUG)
|
||||||
initial_markets = {'ETH/BTC': {}}
|
initial_markets = {'ETH/BTC': {}}
|
||||||
|
|
||||||
@ -371,17 +371,17 @@ def test__reload_markets(default_conf, mocker, caplog):
|
|||||||
assert exchange.markets == initial_markets
|
assert exchange.markets == initial_markets
|
||||||
|
|
||||||
# less than 10 minutes have passed, no reload
|
# less than 10 minutes have passed, no reload
|
||||||
exchange._reload_markets()
|
exchange.reload_markets()
|
||||||
assert exchange.markets == initial_markets
|
assert exchange.markets == initial_markets
|
||||||
|
|
||||||
# more than 10 minutes have passed, reload is executed
|
# more than 10 minutes have passed, reload is executed
|
||||||
exchange._last_markets_refresh = arrow.utcnow().timestamp - 15 * 60
|
exchange._last_markets_refresh = arrow.utcnow().timestamp - 15 * 60
|
||||||
exchange._reload_markets()
|
exchange.reload_markets()
|
||||||
assert exchange.markets == updated_markets
|
assert exchange.markets == updated_markets
|
||||||
assert log_has('Performing scheduled market reload..', caplog)
|
assert log_has('Performing scheduled market reload..', caplog)
|
||||||
|
|
||||||
|
|
||||||
def test__reload_markets_exception(default_conf, mocker, caplog):
|
def test_reload_markets_exception(default_conf, mocker, caplog):
|
||||||
caplog.set_level(logging.DEBUG)
|
caplog.set_level(logging.DEBUG)
|
||||||
|
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
@ -390,7 +390,7 @@ def test__reload_markets_exception(default_conf, mocker, caplog):
|
|||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance")
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance")
|
||||||
|
|
||||||
# less than 10 minutes have passed, no reload
|
# less than 10 minutes have passed, no reload
|
||||||
exchange._reload_markets()
|
exchange.reload_markets()
|
||||||
assert exchange._last_markets_refresh == 0
|
assert exchange._last_markets_refresh == 0
|
||||||
assert log_has_re(r"Could not reload markets.*", caplog)
|
assert log_has_re(r"Could not reload markets.*", caplog)
|
||||||
|
|
||||||
@ -1258,7 +1258,8 @@ def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name):
|
|||||||
|
|
||||||
exchange._async_get_candle_history = Mock(wraps=mock_candle_hist)
|
exchange._async_get_candle_history = Mock(wraps=mock_candle_hist)
|
||||||
# one_call calculation * 1.8 should do 2 calls
|
# one_call calculation * 1.8 should do 2 calls
|
||||||
since = 5 * 60 * 500 * 1.8
|
|
||||||
|
since = 5 * 60 * exchange._ft_has['ohlcv_candle_limit'] * 1.8
|
||||||
ret = exchange.get_historic_ohlcv(pair, "5m", int((arrow.utcnow().timestamp - since) * 1000))
|
ret = exchange.get_historic_ohlcv(pair, "5m", int((arrow.utcnow().timestamp - since) * 1000))
|
||||||
|
|
||||||
assert exchange._async_get_candle_history.call_count == 2
|
assert exchange._async_get_candle_history.call_count == 2
|
||||||
@ -1733,6 +1734,7 @@ def test_cancel_order_dry_run(default_conf, mocker, exchange_name):
|
|||||||
default_conf['dry_run'] = True
|
default_conf['dry_run'] = True
|
||||||
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
|
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
|
||||||
assert exchange.cancel_order(order_id='123', pair='TKN/BTC') == {}
|
assert exchange.cancel_order(order_id='123', pair='TKN/BTC') == {}
|
||||||
|
assert exchange.cancel_stoploss_order(order_id='123', pair='TKN/BTC') == {}
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||||
@ -1817,6 +1819,25 @@ def test_cancel_order(default_conf, mocker, exchange_name):
|
|||||||
order_id='_', pair='TKN/BTC')
|
order_id='_', pair='TKN/BTC')
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||||
|
def test_cancel_stoploss_order(default_conf, mocker, exchange_name):
|
||||||
|
default_conf['dry_run'] = False
|
||||||
|
api_mock = MagicMock()
|
||||||
|
api_mock.cancel_order = MagicMock(return_value=123)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||||
|
assert exchange.cancel_stoploss_order(order_id='_', pair='TKN/BTC') == 123
|
||||||
|
|
||||||
|
with pytest.raises(InvalidOrderException):
|
||||||
|
api_mock.cancel_order = MagicMock(side_effect=ccxt.InvalidOrder("Did not find order"))
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||||
|
exchange.cancel_stoploss_order(order_id='_', pair='TKN/BTC')
|
||||||
|
assert api_mock.cancel_order.call_count == 1
|
||||||
|
|
||||||
|
ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name,
|
||||||
|
"cancel_stoploss_order", "cancel_order",
|
||||||
|
order_id='_', pair='TKN/BTC')
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||||
def test_get_order(default_conf, mocker, exchange_name):
|
def test_get_order(default_conf, mocker, exchange_name):
|
||||||
default_conf['dry_run'] = True
|
default_conf['dry_run'] = True
|
||||||
@ -1846,6 +1867,38 @@ def test_get_order(default_conf, mocker, exchange_name):
|
|||||||
order_id='_', pair='TKN/BTC')
|
order_id='_', pair='TKN/BTC')
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||||
|
def test_get_stoploss_order(default_conf, mocker, exchange_name):
|
||||||
|
# Don't test FTX here - that needs a seperate test
|
||||||
|
if exchange_name == 'ftx':
|
||||||
|
return
|
||||||
|
default_conf['dry_run'] = True
|
||||||
|
order = MagicMock()
|
||||||
|
order.myid = 123
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
|
||||||
|
exchange._dry_run_open_orders['X'] = order
|
||||||
|
assert exchange.get_stoploss_order('X', 'TKN/BTC').myid == 123
|
||||||
|
|
||||||
|
with pytest.raises(InvalidOrderException, match=r'Tried to get an invalid dry-run-order.*'):
|
||||||
|
exchange.get_stoploss_order('Y', 'TKN/BTC')
|
||||||
|
|
||||||
|
default_conf['dry_run'] = False
|
||||||
|
api_mock = MagicMock()
|
||||||
|
api_mock.fetch_order = MagicMock(return_value=456)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||||
|
assert exchange.get_stoploss_order('X', 'TKN/BTC') == 456
|
||||||
|
|
||||||
|
with pytest.raises(InvalidOrderException):
|
||||||
|
api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found"))
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||||
|
exchange.get_stoploss_order(order_id='_', pair='TKN/BTC')
|
||||||
|
assert api_mock.fetch_order.call_count == 1
|
||||||
|
|
||||||
|
ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name,
|
||||||
|
'get_stoploss_order', 'fetch_order',
|
||||||
|
order_id='_', pair='TKN/BTC')
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||||
def test_name(default_conf, mocker, exchange_name):
|
def test_name(default_conf, mocker, exchange_name):
|
||||||
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
|
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
|
||||||
@ -2192,12 +2245,18 @@ def test_extract_cost_curr_rate(mocker, default_conf, order, expected) -> None:
|
|||||||
'fee': {'currency': 'NEO', 'cost': 0.0012}}, 0.001944),
|
'fee': {'currency': 'NEO', 'cost': 0.0012}}, 0.001944),
|
||||||
({'symbol': 'ETH/BTC', 'amount': 2.21, 'cost': 0.02992561,
|
({'symbol': 'ETH/BTC', 'amount': 2.21, 'cost': 0.02992561,
|
||||||
'fee': {'currency': 'NEO', 'cost': 0.00027452}}, 0.00074305),
|
'fee': {'currency': 'NEO', 'cost': 0.00027452}}, 0.00074305),
|
||||||
# TODO: More tests here!
|
|
||||||
# Rate included in return - return as is
|
# Rate included in return - return as is
|
||||||
({'symbol': 'ETH/BTC', 'amount': 0.04, 'cost': 0.05,
|
({'symbol': 'ETH/BTC', 'amount': 0.04, 'cost': 0.05,
|
||||||
'fee': {'currency': 'USDT', 'cost': 0.34, 'rate': 0.01}}, 0.01),
|
'fee': {'currency': 'USDT', 'cost': 0.34, 'rate': 0.01}}, 0.01),
|
||||||
({'symbol': 'ETH/BTC', 'amount': 0.04, 'cost': 0.05,
|
({'symbol': 'ETH/BTC', 'amount': 0.04, 'cost': 0.05,
|
||||||
'fee': {'currency': 'USDT', 'cost': 0.34, 'rate': 0.005}}, 0.005),
|
'fee': {'currency': 'USDT', 'cost': 0.34, 'rate': 0.005}}, 0.005),
|
||||||
|
# 0.1% filled - no costs (kraken - #3431)
|
||||||
|
({'symbol': 'ETH/BTC', 'amount': 0.04, 'cost': 0.0,
|
||||||
|
'fee': {'currency': 'BTC', 'cost': 0.0, 'rate': None}}, None),
|
||||||
|
({'symbol': 'ETH/BTC', 'amount': 0.04, 'cost': 0.0,
|
||||||
|
'fee': {'currency': 'ETH', 'cost': 0.0, 'rate': None}}, 0.0),
|
||||||
|
({'symbol': 'ETH/BTC', 'amount': 0.04, 'cost': 0.0,
|
||||||
|
'fee': {'currency': 'NEO', 'cost': 0.0, 'rate': None}}, None),
|
||||||
])
|
])
|
||||||
def test_calculate_fee_rate(mocker, default_conf, order, expected) -> None:
|
def test_calculate_fee_rate(mocker, default_conf, order, expected) -> None:
|
||||||
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', return_value={'last': 0.081})
|
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', return_value={'last': 0.081})
|
||||||
|
163
tests/exchange/test_ftx.py
Normal file
163
tests/exchange/test_ftx.py
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
# pragma pylint: disable=missing-docstring, C0103, bad-continuation, global-statement
|
||||||
|
# pragma pylint: disable=protected-access
|
||||||
|
from random import randint
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
import ccxt
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from freqtrade.exceptions import (DependencyException, InvalidOrderException,
|
||||||
|
OperationalException, TemporaryError)
|
||||||
|
from tests.conftest import get_patched_exchange
|
||||||
|
from .test_exchange import ccxt_exceptionhandlers
|
||||||
|
|
||||||
|
STOPLOSS_ORDERTYPE = 'stop'
|
||||||
|
|
||||||
|
|
||||||
|
def test_stoploss_order_ftx(default_conf, mocker):
|
||||||
|
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, 'ftx')
|
||||||
|
|
||||||
|
# stoploss_on_exchange_limit_ratio is irrelevant for ftx market orders
|
||||||
|
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190,
|
||||||
|
order_types={'stoploss_on_exchange_limit_ratio': 1.05})
|
||||||
|
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC'
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell'
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['amount'] == 1
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['price'] == 190
|
||||||
|
assert 'orderPrice' not in api_mock.create_order.call_args_list[0][1]['params']
|
||||||
|
|
||||||
|
assert api_mock.create_order.call_count == 1
|
||||||
|
|
||||||
|
api_mock.create_order.reset_mock()
|
||||||
|
|
||||||
|
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||||
|
|
||||||
|
assert 'id' in order
|
||||||
|
assert 'info' in order
|
||||||
|
assert order['id'] == order_id
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC'
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell'
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['amount'] == 1
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['price'] == 220
|
||||||
|
assert 'orderPrice' not in api_mock.create_order.call_args_list[0][1]['params']
|
||||||
|
|
||||||
|
api_mock.create_order.reset_mock()
|
||||||
|
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220,
|
||||||
|
order_types={'stoploss': 'limit'})
|
||||||
|
|
||||||
|
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'] == STOPLOSS_ORDERTYPE
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell'
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['amount'] == 1
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['price'] == 220
|
||||||
|
assert 'orderPrice' in api_mock.create_order.call_args_list[0][1]['params']
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['params']['orderPrice'] == 217.8
|
||||||
|
|
||||||
|
# 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, 'ftx')
|
||||||
|
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("ftx Order would trigger immediately."))
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx')
|
||||||
|
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||||
|
|
||||||
|
with pytest.raises(TemporaryError):
|
||||||
|
api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("No connection"))
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx')
|
||||||
|
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||||
|
|
||||||
|
with pytest.raises(OperationalException, match=r".*DeadBeef.*"):
|
||||||
|
api_mock.create_order = MagicMock(side_effect=ccxt.BaseError("DeadBeef"))
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx')
|
||||||
|
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||||
|
|
||||||
|
|
||||||
|
def test_stoploss_order_dry_run_ftx(default_conf, mocker):
|
||||||
|
api_mock = MagicMock()
|
||||||
|
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, 'ftx')
|
||||||
|
|
||||||
|
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'] == STOPLOSS_ORDERTYPE
|
||||||
|
assert order['price'] == 220
|
||||||
|
assert order['amount'] == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_stoploss_adjust_ftx(mocker, default_conf):
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, id='ftx')
|
||||||
|
order = {
|
||||||
|
'type': STOPLOSS_ORDERTYPE,
|
||||||
|
'price': 1500,
|
||||||
|
}
|
||||||
|
assert exchange.stoploss_adjust(1501, order)
|
||||||
|
assert not exchange.stoploss_adjust(1499, order)
|
||||||
|
# Test with invalid order case ...
|
||||||
|
order['type'] = 'stop_loss_limit'
|
||||||
|
assert not exchange.stoploss_adjust(1501, order)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_stoploss_order(default_conf, mocker):
|
||||||
|
default_conf['dry_run'] = True
|
||||||
|
order = MagicMock()
|
||||||
|
order.myid = 123
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, id='ftx')
|
||||||
|
exchange._dry_run_open_orders['X'] = order
|
||||||
|
assert exchange.get_stoploss_order('X', 'TKN/BTC').myid == 123
|
||||||
|
|
||||||
|
with pytest.raises(InvalidOrderException, match=r'Tried to get an invalid dry-run-order.*'):
|
||||||
|
exchange.get_stoploss_order('Y', 'TKN/BTC')
|
||||||
|
|
||||||
|
default_conf['dry_run'] = False
|
||||||
|
api_mock = MagicMock()
|
||||||
|
api_mock.fetch_orders = MagicMock(return_value=[{'id': 'X', 'status': '456'}])
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, id='ftx')
|
||||||
|
assert exchange.get_stoploss_order('X', 'TKN/BTC')['status'] == '456'
|
||||||
|
|
||||||
|
api_mock.fetch_orders = MagicMock(return_value=[{'id': 'Y', 'status': '456'}])
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, id='ftx')
|
||||||
|
with pytest.raises(InvalidOrderException, match=r"Could not get stoploss order for id X"):
|
||||||
|
exchange.get_stoploss_order('X', 'TKN/BTC')['status']
|
||||||
|
|
||||||
|
with pytest.raises(InvalidOrderException):
|
||||||
|
api_mock.fetch_orders = MagicMock(side_effect=ccxt.InvalidOrder("Order not found"))
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, id='ftx')
|
||||||
|
exchange.get_stoploss_order(order_id='_', pair='TKN/BTC')
|
||||||
|
assert api_mock.fetch_orders.call_count == 1
|
||||||
|
|
||||||
|
ccxt_exceptionhandlers(mocker, default_conf, api_mock, 'ftx',
|
||||||
|
'get_stoploss_order', 'fetch_orders',
|
||||||
|
order_id='_', pair='TKN/BTC')
|
@ -11,6 +11,8 @@ from freqtrade.exceptions import (DependencyException, InvalidOrderException,
|
|||||||
from tests.conftest import get_patched_exchange
|
from tests.conftest import get_patched_exchange
|
||||||
from tests.exchange.test_exchange import ccxt_exceptionhandlers
|
from tests.exchange.test_exchange import ccxt_exceptionhandlers
|
||||||
|
|
||||||
|
STOPLOSS_ORDERTYPE = 'stop-loss'
|
||||||
|
|
||||||
|
|
||||||
def test_buy_kraken_trading_agreement(default_conf, mocker):
|
def test_buy_kraken_trading_agreement(default_conf, mocker):
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
@ -159,7 +161,6 @@ def test_get_balances_prod(default_conf, mocker):
|
|||||||
def test_stoploss_order_kraken(default_conf, mocker):
|
def test_stoploss_order_kraken(default_conf, mocker):
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
|
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
|
||||||
order_type = 'stop-loss'
|
|
||||||
|
|
||||||
api_mock.create_order = MagicMock(return_value={
|
api_mock.create_order = MagicMock(return_value={
|
||||||
'id': order_id,
|
'id': order_id,
|
||||||
@ -187,7 +188,7 @@ def test_stoploss_order_kraken(default_conf, mocker):
|
|||||||
assert 'info' in order
|
assert 'info' in order
|
||||||
assert order['id'] == order_id
|
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]['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]['type'] == STOPLOSS_ORDERTYPE
|
||||||
assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell'
|
assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell'
|
||||||
assert api_mock.create_order.call_args_list[0][1]['amount'] == 1
|
assert api_mock.create_order.call_args_list[0][1]['amount'] == 1
|
||||||
assert api_mock.create_order.call_args_list[0][1]['price'] == 220
|
assert api_mock.create_order.call_args_list[0][1]['price'] == 220
|
||||||
@ -218,7 +219,6 @@ def test_stoploss_order_kraken(default_conf, mocker):
|
|||||||
|
|
||||||
def test_stoploss_order_dry_run_kraken(default_conf, mocker):
|
def test_stoploss_order_dry_run_kraken(default_conf, mocker):
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
order_type = 'stop-loss'
|
|
||||||
default_conf['dry_run'] = True
|
default_conf['dry_run'] = True
|
||||||
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
|
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)
|
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
|
||||||
@ -233,7 +233,7 @@ def test_stoploss_order_dry_run_kraken(default_conf, mocker):
|
|||||||
assert 'info' in order
|
assert 'info' in order
|
||||||
assert 'type' in order
|
assert 'type' in order
|
||||||
|
|
||||||
assert order['type'] == order_type
|
assert order['type'] == STOPLOSS_ORDERTYPE
|
||||||
assert order['price'] == 220
|
assert order['price'] == 220
|
||||||
assert order['amount'] == 1
|
assert order['amount'] == 1
|
||||||
|
|
||||||
@ -241,7 +241,7 @@ def test_stoploss_order_dry_run_kraken(default_conf, mocker):
|
|||||||
def test_stoploss_adjust_kraken(mocker, default_conf):
|
def test_stoploss_adjust_kraken(mocker, default_conf):
|
||||||
exchange = get_patched_exchange(mocker, default_conf, id='kraken')
|
exchange = get_patched_exchange(mocker, default_conf, id='kraken')
|
||||||
order = {
|
order = {
|
||||||
'type': 'stop-loss',
|
'type': STOPLOSS_ORDERTYPE,
|
||||||
'price': 1500,
|
'price': 1500,
|
||||||
}
|
}
|
||||||
assert exchange.stoploss_adjust(1501, order)
|
assert exchange.stoploss_adjust(1501, order)
|
||||||
|
@ -659,17 +659,17 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir):
|
|||||||
mocker.patch('freqtrade.pairlist.pairlistmanager.PairListManager.whitelist',
|
mocker.patch('freqtrade.pairlist.pairlistmanager.PairListManager.whitelist',
|
||||||
PropertyMock(return_value=['UNITTEST/BTC']))
|
PropertyMock(return_value=['UNITTEST/BTC']))
|
||||||
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock)
|
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock)
|
||||||
gen_table_mock = MagicMock()
|
text_table_mock = MagicMock()
|
||||||
sell_reason_mock = MagicMock()
|
sell_reason_mock = MagicMock()
|
||||||
gen_strattable_mock = MagicMock()
|
strattable_mock = MagicMock()
|
||||||
gen_strat_summary = MagicMock()
|
strat_summary = MagicMock()
|
||||||
|
|
||||||
mocker.patch.multiple('freqtrade.optimize.optimize_reports',
|
mocker.patch.multiple('freqtrade.optimize.optimize_reports',
|
||||||
generate_text_table=gen_table_mock,
|
text_table_bt_results=text_table_mock,
|
||||||
generate_text_table_strategy=gen_strattable_mock,
|
text_table_strategy=strattable_mock,
|
||||||
generate_pair_metrics=MagicMock(),
|
generate_pair_metrics=MagicMock(),
|
||||||
generate_sell_reason_stats=sell_reason_mock,
|
generate_sell_reason_stats=sell_reason_mock,
|
||||||
generate_strategy_metrics=gen_strat_summary,
|
generate_strategy_metrics=strat_summary,
|
||||||
)
|
)
|
||||||
patched_configuration_load_config_file(mocker, default_conf)
|
patched_configuration_load_config_file(mocker, default_conf)
|
||||||
|
|
||||||
@ -690,10 +690,10 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir):
|
|||||||
start_backtesting(args)
|
start_backtesting(args)
|
||||||
# 2 backtests, 4 tables
|
# 2 backtests, 4 tables
|
||||||
assert backtestmock.call_count == 2
|
assert backtestmock.call_count == 2
|
||||||
assert gen_table_mock.call_count == 4
|
assert text_table_mock.call_count == 4
|
||||||
assert gen_strattable_mock.call_count == 1
|
assert strattable_mock.call_count == 1
|
||||||
assert sell_reason_mock.call_count == 2
|
assert sell_reason_mock.call_count == 2
|
||||||
assert gen_strat_summary.call_count == 1
|
assert strat_summary.call_count == 1
|
||||||
|
|
||||||
# check the logs, that will contain the backtest result
|
# check the logs, that will contain the backtest result
|
||||||
exists = [
|
exists = [
|
||||||
|
@ -7,13 +7,13 @@ from arrow import Arrow
|
|||||||
from freqtrade.edge import PairInfo
|
from freqtrade.edge import PairInfo
|
||||||
from freqtrade.optimize.optimize_reports import (
|
from freqtrade.optimize.optimize_reports import (
|
||||||
generate_pair_metrics, generate_edge_table, generate_sell_reason_stats,
|
generate_pair_metrics, generate_edge_table, generate_sell_reason_stats,
|
||||||
generate_text_table, generate_text_table_sell_reason, generate_strategy_metrics,
|
text_table_bt_results, text_table_sell_reason, generate_strategy_metrics,
|
||||||
generate_text_table_strategy, store_backtest_result)
|
text_table_strategy, store_backtest_result)
|
||||||
from freqtrade.strategy.interface import SellType
|
from freqtrade.strategy.interface import SellType
|
||||||
from tests.conftest import patch_exchange
|
from tests.conftest import patch_exchange
|
||||||
|
|
||||||
|
|
||||||
def test_generate_text_table(default_conf, mocker):
|
def test_text_table_bt_results(default_conf, mocker):
|
||||||
|
|
||||||
results = pd.DataFrame(
|
results = pd.DataFrame(
|
||||||
{
|
{
|
||||||
@ -40,8 +40,7 @@ def test_generate_text_table(default_conf, mocker):
|
|||||||
|
|
||||||
pair_results = generate_pair_metrics(data={'ETH/BTC': {}}, stake_currency='BTC',
|
pair_results = generate_pair_metrics(data={'ETH/BTC': {}}, stake_currency='BTC',
|
||||||
max_open_trades=2, results=results)
|
max_open_trades=2, results=results)
|
||||||
assert generate_text_table(pair_results,
|
assert text_table_bt_results(pair_results, stake_currency='BTC') == result_str
|
||||||
stake_currency='BTC') == result_str
|
|
||||||
|
|
||||||
|
|
||||||
def test_generate_pair_metrics(default_conf, mocker):
|
def test_generate_pair_metrics(default_conf, mocker):
|
||||||
@ -69,7 +68,7 @@ def test_generate_pair_metrics(default_conf, mocker):
|
|||||||
pytest.approx(pair_results[-1]['profit_sum_pct']) == pair_results[-1]['profit_sum'] * 100)
|
pytest.approx(pair_results[-1]['profit_sum_pct']) == pair_results[-1]['profit_sum'] * 100)
|
||||||
|
|
||||||
|
|
||||||
def test_generate_text_table_sell_reason(default_conf):
|
def test_text_table_sell_reason(default_conf):
|
||||||
|
|
||||||
results = pd.DataFrame(
|
results = pd.DataFrame(
|
||||||
{
|
{
|
||||||
@ -97,7 +96,7 @@ def test_generate_text_table_sell_reason(default_conf):
|
|||||||
|
|
||||||
sell_reason_stats = generate_sell_reason_stats(max_open_trades=2,
|
sell_reason_stats = generate_sell_reason_stats(max_open_trades=2,
|
||||||
results=results)
|
results=results)
|
||||||
assert generate_text_table_sell_reason(sell_reason_stats=sell_reason_stats,
|
assert text_table_sell_reason(sell_reason_stats=sell_reason_stats,
|
||||||
stake_currency='BTC') == result_str
|
stake_currency='BTC') == result_str
|
||||||
|
|
||||||
|
|
||||||
@ -136,7 +135,7 @@ def test_generate_sell_reason_stats(default_conf):
|
|||||||
assert stop_result['profit_mean_pct'] == round(stop_result['profit_mean'] * 100, 2)
|
assert stop_result['profit_mean_pct'] == round(stop_result['profit_mean'] * 100, 2)
|
||||||
|
|
||||||
|
|
||||||
def test_generate_text_table_strategy(default_conf, mocker):
|
def test_text_table_strategy(default_conf, mocker):
|
||||||
results = {}
|
results = {}
|
||||||
results['TestStrategy1'] = pd.DataFrame(
|
results['TestStrategy1'] = pd.DataFrame(
|
||||||
{
|
{
|
||||||
@ -178,7 +177,7 @@ def test_generate_text_table_strategy(default_conf, mocker):
|
|||||||
max_open_trades=2,
|
max_open_trades=2,
|
||||||
all_results=results)
|
all_results=results)
|
||||||
|
|
||||||
assert generate_text_table_strategy(strategy_results, 'BTC') == result_str
|
assert text_table_strategy(strategy_results, 'BTC') == result_str
|
||||||
|
|
||||||
|
|
||||||
def test_generate_edge_table(edge_conf, mocker):
|
def test_generate_edge_table(edge_conf, mocker):
|
||||||
|
@ -421,6 +421,23 @@ def test__whitelist_for_active_markets(mocker, whitelist_conf, markets, pairlist
|
|||||||
assert log_message in caplog.text
|
assert log_message in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("pairlist", AVAILABLE_PAIRLISTS)
|
||||||
|
def test__whitelist_for_active_markets_empty(mocker, whitelist_conf, markets, pairlist, tickers):
|
||||||
|
whitelist_conf['pairlists'][0]['method'] = pairlist
|
||||||
|
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True)
|
||||||
|
|
||||||
|
freqtrade = get_patched_freqtradebot(mocker, whitelist_conf)
|
||||||
|
mocker.patch.multiple('freqtrade.exchange.Exchange',
|
||||||
|
markets=PropertyMock(return_value=None),
|
||||||
|
get_tickers=tickers
|
||||||
|
)
|
||||||
|
# Assign starting whitelist
|
||||||
|
pairlist_handler = freqtrade.pairlists._pairlist_handlers[0]
|
||||||
|
with pytest.raises(OperationalException, match=r'Markets not loaded.*'):
|
||||||
|
pairlist_handler._whitelist_for_active_markets(['ETH/BTC'])
|
||||||
|
|
||||||
|
|
||||||
def test_volumepairlist_invalid_sortvalue(mocker, markets, whitelist_conf):
|
def test_volumepairlist_invalid_sortvalue(mocker, markets, whitelist_conf):
|
||||||
whitelist_conf['pairlists'][0].update({"sort_key": "asdf"})
|
whitelist_conf['pairlists'][0].update({"sort_key": "asdf"})
|
||||||
|
|
||||||
|
@ -42,8 +42,12 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
|||||||
rpc._rpc_trade_status()
|
rpc._rpc_trade_status()
|
||||||
|
|
||||||
freqtradebot.enter_positions()
|
freqtradebot.enter_positions()
|
||||||
|
trades = Trade.get_open_trades()
|
||||||
|
trades[0].open_order_id = None
|
||||||
|
freqtradebot.exit_positions(trades)
|
||||||
|
|
||||||
results = rpc._rpc_trade_status()
|
results = rpc._rpc_trade_status()
|
||||||
assert {
|
assert results[0] == {
|
||||||
'trade_id': 1,
|
'trade_id': 1,
|
||||||
'pair': 'ETH/BTC',
|
'pair': 'ETH/BTC',
|
||||||
'base_currency': 'BTC',
|
'base_currency': 'BTC',
|
||||||
@ -54,11 +58,11 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
|||||||
'fee_open': ANY,
|
'fee_open': ANY,
|
||||||
'fee_open_cost': ANY,
|
'fee_open_cost': ANY,
|
||||||
'fee_open_currency': ANY,
|
'fee_open_currency': ANY,
|
||||||
'fee_close': ANY,
|
'fee_close': fee.return_value,
|
||||||
'fee_close_cost': ANY,
|
'fee_close_cost': ANY,
|
||||||
'fee_close_currency': ANY,
|
'fee_close_currency': ANY,
|
||||||
'open_rate_requested': ANY,
|
'open_rate_requested': ANY,
|
||||||
'open_trade_price': ANY,
|
'open_trade_price': 0.0010025,
|
||||||
'close_rate_requested': ANY,
|
'close_rate_requested': ANY,
|
||||||
'sell_reason': ANY,
|
'sell_reason': ANY,
|
||||||
'sell_order_status': ANY,
|
'sell_order_status': ANY,
|
||||||
@ -81,28 +85,32 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
|||||||
'close_profit_abs': None,
|
'close_profit_abs': None,
|
||||||
'current_profit': -0.00408133,
|
'current_profit': -0.00408133,
|
||||||
'current_profit_pct': -0.41,
|
'current_profit_pct': -0.41,
|
||||||
'stop_loss': 0.0,
|
'current_profit_abs': -4.09e-06,
|
||||||
'stop_loss_abs': 0.0,
|
'stop_loss': 9.882e-06,
|
||||||
'stop_loss_pct': None,
|
'stop_loss_abs': 9.882e-06,
|
||||||
'stop_loss_ratio': None,
|
'stop_loss_pct': -10.0,
|
||||||
|
'stop_loss_ratio': -0.1,
|
||||||
'stoploss_order_id': None,
|
'stoploss_order_id': None,
|
||||||
'stoploss_last_update': None,
|
'stoploss_last_update': ANY,
|
||||||
'stoploss_last_update_timestamp': None,
|
'stoploss_last_update_timestamp': ANY,
|
||||||
'initial_stop_loss': 0.0,
|
'initial_stop_loss': 9.882e-06,
|
||||||
'initial_stop_loss_abs': 0.0,
|
'initial_stop_loss_abs': 9.882e-06,
|
||||||
'initial_stop_loss_pct': None,
|
'initial_stop_loss_pct': -10.0,
|
||||||
'initial_stop_loss_ratio': None,
|
'initial_stop_loss_ratio': -0.1,
|
||||||
'open_order': '(limit buy rem=0.00000000)',
|
'stoploss_current_dist': -1.1080000000000002e-06,
|
||||||
|
'stoploss_current_dist_ratio': -0.10081893,
|
||||||
|
'stoploss_entry_dist': -0.00010475,
|
||||||
|
'stoploss_entry_dist_ratio': -0.10448878,
|
||||||
|
'open_order': None,
|
||||||
'exchange': 'bittrex',
|
'exchange': 'bittrex',
|
||||||
|
}
|
||||||
} == results[0]
|
|
||||||
|
|
||||||
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate',
|
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate',
|
||||||
MagicMock(side_effect=DependencyException("Pair 'ETH/BTC' not available")))
|
MagicMock(side_effect=DependencyException("Pair 'ETH/BTC' not available")))
|
||||||
results = rpc._rpc_trade_status()
|
results = rpc._rpc_trade_status()
|
||||||
assert isnan(results[0]['current_profit'])
|
assert isnan(results[0]['current_profit'])
|
||||||
assert isnan(results[0]['current_rate'])
|
assert isnan(results[0]['current_rate'])
|
||||||
assert {
|
assert results[0] == {
|
||||||
'trade_id': 1,
|
'trade_id': 1,
|
||||||
'pair': 'ETH/BTC',
|
'pair': 'ETH/BTC',
|
||||||
'base_currency': 'BTC',
|
'base_currency': 'BTC',
|
||||||
@ -113,7 +121,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
|||||||
'fee_open': ANY,
|
'fee_open': ANY,
|
||||||
'fee_open_cost': ANY,
|
'fee_open_cost': ANY,
|
||||||
'fee_open_currency': ANY,
|
'fee_open_currency': ANY,
|
||||||
'fee_close': ANY,
|
'fee_close': fee.return_value,
|
||||||
'fee_close_cost': ANY,
|
'fee_close_cost': ANY,
|
||||||
'fee_close_currency': ANY,
|
'fee_close_currency': ANY,
|
||||||
'open_rate_requested': ANY,
|
'open_rate_requested': ANY,
|
||||||
@ -140,20 +148,25 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
|||||||
'close_profit_abs': None,
|
'close_profit_abs': None,
|
||||||
'current_profit': ANY,
|
'current_profit': ANY,
|
||||||
'current_profit_pct': ANY,
|
'current_profit_pct': ANY,
|
||||||
'stop_loss': 0.0,
|
'current_profit_abs': ANY,
|
||||||
'stop_loss_abs': 0.0,
|
'stop_loss': 9.882e-06,
|
||||||
'stop_loss_pct': None,
|
'stop_loss_abs': 9.882e-06,
|
||||||
'stop_loss_ratio': None,
|
'stop_loss_pct': -10.0,
|
||||||
|
'stop_loss_ratio': -0.1,
|
||||||
'stoploss_order_id': None,
|
'stoploss_order_id': None,
|
||||||
'stoploss_last_update': None,
|
'stoploss_last_update': ANY,
|
||||||
'stoploss_last_update_timestamp': None,
|
'stoploss_last_update_timestamp': ANY,
|
||||||
'initial_stop_loss': 0.0,
|
'initial_stop_loss': 9.882e-06,
|
||||||
'initial_stop_loss_abs': 0.0,
|
'initial_stop_loss_abs': 9.882e-06,
|
||||||
'initial_stop_loss_pct': None,
|
'initial_stop_loss_pct': -10.0,
|
||||||
'initial_stop_loss_ratio': None,
|
'initial_stop_loss_ratio': -0.1,
|
||||||
'open_order': '(limit buy rem=0.00000000)',
|
'stoploss_current_dist': ANY,
|
||||||
|
'stoploss_current_dist_ratio': ANY,
|
||||||
|
'stoploss_entry_dist': -0.00010475,
|
||||||
|
'stoploss_entry_dist_ratio': -0.10448878,
|
||||||
|
'open_order': None,
|
||||||
'exchange': 'bittrex',
|
'exchange': 'bittrex',
|
||||||
} == results[0]
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None:
|
def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None:
|
||||||
@ -581,7 +594,7 @@ def test_rpc_stopbuy(mocker, default_conf) -> None:
|
|||||||
|
|
||||||
assert freqtradebot.config['max_open_trades'] != 0
|
assert freqtradebot.config['max_open_trades'] != 0
|
||||||
result = rpc._rpc_stopbuy()
|
result = rpc._rpc_stopbuy()
|
||||||
assert {'status': 'No more buy will occur from now. Run /reload_conf to reset.'} == result
|
assert {'status': 'No more buy will occur from now. Run /reload_config to reset.'} == result
|
||||||
assert freqtradebot.config['max_open_trades'] == 0
|
assert freqtradebot.config['max_open_trades'] == 0
|
||||||
|
|
||||||
|
|
||||||
|
@ -251,10 +251,10 @@ def test_api_cleanup(default_conf, mocker, caplog):
|
|||||||
def test_api_reloadconf(botclient):
|
def test_api_reloadconf(botclient):
|
||||||
ftbot, client = botclient
|
ftbot, client = botclient
|
||||||
|
|
||||||
rc = client_post(client, f"{BASE_URI}/reload_conf")
|
rc = client_post(client, f"{BASE_URI}/reload_config")
|
||||||
assert_response(rc)
|
assert_response(rc)
|
||||||
assert rc.json == {'status': 'reloading config ...'}
|
assert rc.json == {'status': 'reloading config ...'}
|
||||||
assert ftbot.state == State.RELOAD_CONF
|
assert ftbot.state == State.RELOAD_CONFIG
|
||||||
|
|
||||||
|
|
||||||
def test_api_stopbuy(botclient):
|
def test_api_stopbuy(botclient):
|
||||||
@ -263,7 +263,7 @@ def test_api_stopbuy(botclient):
|
|||||||
|
|
||||||
rc = client_post(client, f"{BASE_URI}/stopbuy")
|
rc = client_post(client, f"{BASE_URI}/stopbuy")
|
||||||
assert_response(rc)
|
assert_response(rc)
|
||||||
assert rc.json == {'status': 'No more buy will occur from now. Run /reload_conf to reset.'}
|
assert rc.json == {'status': 'No more buy will occur from now. Run /reload_config to reset.'}
|
||||||
assert ftbot.config['max_open_trades'] == 0
|
assert ftbot.config['max_open_trades'] == 0
|
||||||
|
|
||||||
|
|
||||||
@ -326,6 +326,8 @@ def test_api_show_config(botclient, mocker):
|
|||||||
assert rc.json['timeframe'] == '5m'
|
assert rc.json['timeframe'] == '5m'
|
||||||
assert rc.json['state'] == 'running'
|
assert rc.json['state'] == 'running'
|
||||||
assert not rc.json['trailing_stop']
|
assert not rc.json['trailing_stop']
|
||||||
|
assert 'bid_strategy' in rc.json
|
||||||
|
assert 'ask_strategy' in rc.json
|
||||||
|
|
||||||
|
|
||||||
def test_api_daily(botclient, mocker, ticker, fee, markets):
|
def test_api_daily(botclient, mocker, ticker, fee, markets):
|
||||||
@ -429,9 +431,17 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, li
|
|||||||
'profit_all_coin': 6.217e-05,
|
'profit_all_coin': 6.217e-05,
|
||||||
'profit_all_fiat': 0,
|
'profit_all_fiat': 0,
|
||||||
'profit_all_percent': 6.2,
|
'profit_all_percent': 6.2,
|
||||||
|
'profit_all_percent_mean': 6.2,
|
||||||
|
'profit_all_ratio_mean': 0.06201058,
|
||||||
|
'profit_all_percent_sum': 6.2,
|
||||||
|
'profit_all_ratio_sum': 0.06201058,
|
||||||
'profit_closed_coin': 6.217e-05,
|
'profit_closed_coin': 6.217e-05,
|
||||||
'profit_closed_fiat': 0,
|
'profit_closed_fiat': 0,
|
||||||
'profit_closed_percent': 6.2,
|
'profit_closed_percent': 6.2,
|
||||||
|
'profit_closed_ratio_mean': 0.06201058,
|
||||||
|
'profit_closed_percent_mean': 6.2,
|
||||||
|
'profit_closed_ratio_sum': 0.06201058,
|
||||||
|
'profit_closed_percent_sum': 6.2,
|
||||||
'trade_count': 1,
|
'trade_count': 1,
|
||||||
'closed_trade_count': 1,
|
'closed_trade_count': 1,
|
||||||
}
|
}
|
||||||
@ -496,6 +506,10 @@ def test_api_status(botclient, mocker, ticker, fee, markets):
|
|||||||
assert rc.json == []
|
assert rc.json == []
|
||||||
|
|
||||||
ftbot.enter_positions()
|
ftbot.enter_positions()
|
||||||
|
trades = Trade.get_open_trades()
|
||||||
|
trades[0].open_order_id = None
|
||||||
|
ftbot.exit_positions(trades)
|
||||||
|
|
||||||
rc = client_get(client, f"{BASE_URI}/status")
|
rc = client_get(client, f"{BASE_URI}/status")
|
||||||
assert_response(rc)
|
assert_response(rc)
|
||||||
assert len(rc.json) == 1
|
assert len(rc.json) == 1
|
||||||
@ -510,25 +524,30 @@ def test_api_status(botclient, mocker, ticker, fee, markets):
|
|||||||
'close_rate': None,
|
'close_rate': None,
|
||||||
'current_profit': -0.00408133,
|
'current_profit': -0.00408133,
|
||||||
'current_profit_pct': -0.41,
|
'current_profit_pct': -0.41,
|
||||||
|
'current_profit_abs': -4.09e-06,
|
||||||
'current_rate': 1.099e-05,
|
'current_rate': 1.099e-05,
|
||||||
'open_date': ANY,
|
'open_date': ANY,
|
||||||
'open_date_hum': 'just now',
|
'open_date_hum': 'just now',
|
||||||
'open_timestamp': ANY,
|
'open_timestamp': ANY,
|
||||||
'open_order': '(limit buy rem=0.00000000)',
|
'open_order': None,
|
||||||
'open_rate': 1.098e-05,
|
'open_rate': 1.098e-05,
|
||||||
'pair': 'ETH/BTC',
|
'pair': 'ETH/BTC',
|
||||||
'stake_amount': 0.001,
|
'stake_amount': 0.001,
|
||||||
'stop_loss': 0.0,
|
'stop_loss': 9.882e-06,
|
||||||
'stop_loss_abs': 0.0,
|
'stop_loss_abs': 9.882e-06,
|
||||||
'stop_loss_pct': None,
|
'stop_loss_pct': -10.0,
|
||||||
'stop_loss_ratio': None,
|
'stop_loss_ratio': -0.1,
|
||||||
'stoploss_order_id': None,
|
'stoploss_order_id': None,
|
||||||
'stoploss_last_update': None,
|
'stoploss_last_update': ANY,
|
||||||
'stoploss_last_update_timestamp': None,
|
'stoploss_last_update_timestamp': ANY,
|
||||||
'initial_stop_loss': 0.0,
|
'initial_stop_loss': 9.882e-06,
|
||||||
'initial_stop_loss_abs': 0.0,
|
'initial_stop_loss_abs': 9.882e-06,
|
||||||
'initial_stop_loss_pct': None,
|
'initial_stop_loss_pct': -10.0,
|
||||||
'initial_stop_loss_ratio': None,
|
'initial_stop_loss_ratio': -0.1,
|
||||||
|
'stoploss_current_dist': -1.1080000000000002e-06,
|
||||||
|
'stoploss_current_dist_ratio': -0.10081893,
|
||||||
|
'stoploss_entry_dist': -0.00010475,
|
||||||
|
'stoploss_entry_dist_ratio': -0.10448878,
|
||||||
'trade_id': 1,
|
'trade_id': 1,
|
||||||
'close_rate_requested': None,
|
'close_rate_requested': None,
|
||||||
'current_rate': 1.099e-05,
|
'current_rate': 1.099e-05,
|
||||||
@ -540,9 +559,9 @@ def test_api_status(botclient, mocker, ticker, fee, markets):
|
|||||||
'fee_open_currency': None,
|
'fee_open_currency': None,
|
||||||
'open_date': ANY,
|
'open_date': ANY,
|
||||||
'is_open': True,
|
'is_open': True,
|
||||||
'max_rate': 0.0,
|
'max_rate': 1.099e-05,
|
||||||
'min_rate': None,
|
'min_rate': 1.098e-05,
|
||||||
'open_order_id': ANY,
|
'open_order_id': None,
|
||||||
'open_rate_requested': 1.098e-05,
|
'open_rate_requested': 1.098e-05,
|
||||||
'open_trade_price': 0.0010025,
|
'open_trade_price': 0.0010025,
|
||||||
'sell_reason': None,
|
'sell_reason': None,
|
||||||
|
@ -71,10 +71,11 @@ def test_init(default_conf, mocker, caplog) -> None:
|
|||||||
assert start_polling.dispatcher.add_handler.call_count > 0
|
assert start_polling.dispatcher.add_handler.call_count > 0
|
||||||
assert start_polling.start_polling.call_count == 1
|
assert start_polling.start_polling.call_count == 1
|
||||||
|
|
||||||
message_str = "rpc.telegram is listening for following commands: [['status'], ['profit'], " \
|
message_str = ("rpc.telegram is listening for following commands: [['status'], ['profit'], "
|
||||||
"['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], " \
|
"['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], "
|
||||||
"['performance'], ['daily'], ['count'], ['reload_conf'], ['show_config'], " \
|
"['performance'], ['daily'], ['count'], ['reload_config', 'reload_conf'], "
|
||||||
"['stopbuy'], ['whitelist'], ['blacklist'], ['edge'], ['help'], ['version']]"
|
"['show_config', 'show_conf'], ['stopbuy'], ['whitelist'], ['blacklist'], "
|
||||||
|
"['edge'], ['help'], ['version']]")
|
||||||
|
|
||||||
assert log_has(message_str, caplog)
|
assert log_has(message_str, caplog)
|
||||||
|
|
||||||
@ -434,7 +435,8 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
|
|||||||
assert msg_mock.call_count == 1
|
assert msg_mock.call_count == 1
|
||||||
assert 'No closed trade' in msg_mock.call_args_list[-1][0][0]
|
assert 'No closed trade' in msg_mock.call_args_list[-1][0][0]
|
||||||
assert '*ROI:* All trades' in msg_mock.call_args_list[-1][0][0]
|
assert '*ROI:* All trades' in msg_mock.call_args_list[-1][0][0]
|
||||||
assert '∙ `-0.00000500 BTC (-0.50%)`' in msg_mock.call_args_list[-1][0][0]
|
assert ('∙ `-0.00000500 BTC (-0.50%) (-0.5 \N{GREEK CAPITAL LETTER SIGMA}%)`'
|
||||||
|
in msg_mock.call_args_list[-1][0][0])
|
||||||
msg_mock.reset_mock()
|
msg_mock.reset_mock()
|
||||||
|
|
||||||
# Update the ticker with a market going up
|
# Update the ticker with a market going up
|
||||||
@ -447,10 +449,12 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
|
|||||||
telegram._profit(update=update, context=MagicMock())
|
telegram._profit(update=update, context=MagicMock())
|
||||||
assert msg_mock.call_count == 1
|
assert msg_mock.call_count == 1
|
||||||
assert '*ROI:* Closed trades' in msg_mock.call_args_list[-1][0][0]
|
assert '*ROI:* Closed trades' in msg_mock.call_args_list[-1][0][0]
|
||||||
assert '∙ `0.00006217 BTC (6.20%)`' in msg_mock.call_args_list[-1][0][0]
|
assert ('∙ `0.00006217 BTC (6.20%) (6.2 \N{GREEK CAPITAL LETTER SIGMA}%)`'
|
||||||
|
in msg_mock.call_args_list[-1][0][0])
|
||||||
assert '∙ `0.933 USD`' in msg_mock.call_args_list[-1][0][0]
|
assert '∙ `0.933 USD`' in msg_mock.call_args_list[-1][0][0]
|
||||||
assert '*ROI:* All trades' in msg_mock.call_args_list[-1][0][0]
|
assert '*ROI:* All trades' in msg_mock.call_args_list[-1][0][0]
|
||||||
assert '∙ `0.00006217 BTC (6.20%)`' in msg_mock.call_args_list[-1][0][0]
|
assert ('∙ `0.00006217 BTC (6.20%) (6.2 \N{GREEK CAPITAL LETTER SIGMA}%)`'
|
||||||
|
in msg_mock.call_args_list[-1][0][0])
|
||||||
assert '∙ `0.933 USD`' in msg_mock.call_args_list[-1][0][0]
|
assert '∙ `0.933 USD`' in msg_mock.call_args_list[-1][0][0]
|
||||||
|
|
||||||
assert '*Best Performing:* `ETH/BTC: 6.20%`' in msg_mock.call_args_list[-1][0][0]
|
assert '*Best Performing:* `ETH/BTC: 6.20%`' in msg_mock.call_args_list[-1][0][0]
|
||||||
@ -663,11 +667,11 @@ def test_stopbuy_handle(default_conf, update, mocker) -> None:
|
|||||||
telegram._stopbuy(update=update, context=MagicMock())
|
telegram._stopbuy(update=update, context=MagicMock())
|
||||||
assert freqtradebot.config['max_open_trades'] == 0
|
assert freqtradebot.config['max_open_trades'] == 0
|
||||||
assert msg_mock.call_count == 1
|
assert msg_mock.call_count == 1
|
||||||
assert 'No more buy will occur from now. Run /reload_conf to reset.' \
|
assert 'No more buy will occur from now. Run /reload_config to reset.' \
|
||||||
in msg_mock.call_args_list[0][0][0]
|
in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
|
|
||||||
def test_reload_conf_handle(default_conf, update, mocker) -> None:
|
def test_reload_config_handle(default_conf, update, mocker) -> None:
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.rpc.telegram.Telegram',
|
'freqtrade.rpc.telegram.Telegram',
|
||||||
@ -680,8 +684,8 @@ def test_reload_conf_handle(default_conf, update, mocker) -> None:
|
|||||||
|
|
||||||
freqtradebot.state = State.RUNNING
|
freqtradebot.state = State.RUNNING
|
||||||
assert freqtradebot.state == State.RUNNING
|
assert freqtradebot.state == State.RUNNING
|
||||||
telegram._reload_conf(update=update, context=MagicMock())
|
telegram._reload_config(update=update, context=MagicMock())
|
||||||
assert freqtradebot.state == State.RELOAD_CONF
|
assert freqtradebot.state == State.RELOAD_CONFIG
|
||||||
assert msg_mock.call_count == 1
|
assert msg_mock.call_count == 1
|
||||||
assert 'reloading config' in msg_mock.call_args_list[0][0][0]
|
assert 'reloading config' in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
@ -1013,9 +1017,8 @@ def test_count_handle(default_conf, update, ticker, fee, mocker) -> None:
|
|||||||
msg_mock.reset_mock()
|
msg_mock.reset_mock()
|
||||||
telegram._count(update=update, context=MagicMock())
|
telegram._count(update=update, context=MagicMock())
|
||||||
|
|
||||||
msg = '<pre> current max total stake\n--------- ----- -------------\n' \
|
msg = ('<pre> current max total stake\n--------- ----- -------------\n'
|
||||||
' 1 {} {}</pre>'\
|
' 1 {} {}</pre>').format(
|
||||||
.format(
|
|
||||||
default_conf['max_open_trades'],
|
default_conf['max_open_trades'],
|
||||||
default_conf['stake_amount']
|
default_conf['stake_amount']
|
||||||
)
|
)
|
||||||
@ -1222,7 +1225,7 @@ def test_send_msg_buy_notification(default_conf, mocker) -> None:
|
|||||||
'open_date': arrow.utcnow().shift(hours=-1)
|
'open_date': arrow.utcnow().shift(hours=-1)
|
||||||
})
|
})
|
||||||
assert msg_mock.call_args[0][0] \
|
assert msg_mock.call_args[0][0] \
|
||||||
== '*Bittrex:* Buying ETH/BTC\n' \
|
== '\N{LARGE BLUE CIRCLE} *Bittrex:* Buying ETH/BTC\n' \
|
||||||
'*Amount:* `1333.33333333`\n' \
|
'*Amount:* `1333.33333333`\n' \
|
||||||
'*Open Rate:* `0.00001099`\n' \
|
'*Open Rate:* `0.00001099`\n' \
|
||||||
'*Current Rate:* `0.00001099`\n' \
|
'*Current Rate:* `0.00001099`\n' \
|
||||||
@ -1244,7 +1247,7 @@ def test_send_msg_buy_cancel_notification(default_conf, mocker) -> None:
|
|||||||
'pair': 'ETH/BTC',
|
'pair': 'ETH/BTC',
|
||||||
})
|
})
|
||||||
assert msg_mock.call_args[0][0] \
|
assert msg_mock.call_args[0][0] \
|
||||||
== ('*Bittrex:* Cancelling Open Buy Order for ETH/BTC')
|
== ('\N{WARNING SIGN} *Bittrex:* Cancelling Open Buy Order for ETH/BTC')
|
||||||
|
|
||||||
|
|
||||||
def test_send_msg_sell_notification(default_conf, mocker) -> None:
|
def test_send_msg_sell_notification(default_conf, mocker) -> None:
|
||||||
@ -1277,7 +1280,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
|
|||||||
'close_date': arrow.utcnow(),
|
'close_date': arrow.utcnow(),
|
||||||
})
|
})
|
||||||
assert msg_mock.call_args[0][0] \
|
assert msg_mock.call_args[0][0] \
|
||||||
== ('*Binance:* Selling KEY/ETH\n'
|
== ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH\n'
|
||||||
'*Amount:* `1333.33333333`\n'
|
'*Amount:* `1333.33333333`\n'
|
||||||
'*Open Rate:* `0.00007500`\n'
|
'*Open Rate:* `0.00007500`\n'
|
||||||
'*Current Rate:* `0.00003201`\n'
|
'*Current Rate:* `0.00003201`\n'
|
||||||
@ -1305,7 +1308,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
|
|||||||
'close_date': arrow.utcnow(),
|
'close_date': arrow.utcnow(),
|
||||||
})
|
})
|
||||||
assert msg_mock.call_args[0][0] \
|
assert msg_mock.call_args[0][0] \
|
||||||
== ('*Binance:* Selling KEY/ETH\n'
|
== ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH\n'
|
||||||
'*Amount:* `1333.33333333`\n'
|
'*Amount:* `1333.33333333`\n'
|
||||||
'*Open Rate:* `0.00007500`\n'
|
'*Open Rate:* `0.00007500`\n'
|
||||||
'*Current Rate:* `0.00003201`\n'
|
'*Current Rate:* `0.00003201`\n'
|
||||||
@ -1335,7 +1338,8 @@ def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None:
|
|||||||
'reason': 'Cancelled on exchange'
|
'reason': 'Cancelled on exchange'
|
||||||
})
|
})
|
||||||
assert msg_mock.call_args[0][0] \
|
assert msg_mock.call_args[0][0] \
|
||||||
== ('*Binance:* Cancelling Open Sell Order for KEY/ETH. Reason: Cancelled on exchange')
|
== ('\N{WARNING SIGN} *Binance:* Cancelling Open Sell Order for KEY/ETH. '
|
||||||
|
'Reason: Cancelled on exchange')
|
||||||
|
|
||||||
msg_mock.reset_mock()
|
msg_mock.reset_mock()
|
||||||
telegram.send_msg({
|
telegram.send_msg({
|
||||||
@ -1345,7 +1349,7 @@ def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None:
|
|||||||
'reason': 'timeout'
|
'reason': 'timeout'
|
||||||
})
|
})
|
||||||
assert msg_mock.call_args[0][0] \
|
assert msg_mock.call_args[0][0] \
|
||||||
== ('*Binance:* Cancelling Open Sell Order for KEY/ETH. Reason: timeout')
|
== ('\N{WARNING SIGN} *Binance:* Cancelling Open Sell Order for KEY/ETH. Reason: timeout')
|
||||||
# Reset singleton function to avoid random breaks
|
# Reset singleton function to avoid random breaks
|
||||||
telegram._fiat_converter.convert_amount = old_convamount
|
telegram._fiat_converter.convert_amount = old_convamount
|
||||||
|
|
||||||
@ -1379,7 +1383,7 @@ def test_warning_notification(default_conf, mocker) -> None:
|
|||||||
'type': RPCMessageType.WARNING_NOTIFICATION,
|
'type': RPCMessageType.WARNING_NOTIFICATION,
|
||||||
'status': 'message'
|
'status': 'message'
|
||||||
})
|
})
|
||||||
assert msg_mock.call_args[0][0] == '*Warning:* `message`'
|
assert msg_mock.call_args[0][0] == '\N{WARNING SIGN} *Warning:* `message`'
|
||||||
|
|
||||||
|
|
||||||
def test_custom_notification(default_conf, mocker) -> None:
|
def test_custom_notification(default_conf, mocker) -> None:
|
||||||
@ -1437,12 +1441,11 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None:
|
|||||||
'amount': 1333.3333333333335,
|
'amount': 1333.3333333333335,
|
||||||
'open_date': arrow.utcnow().shift(hours=-1)
|
'open_date': arrow.utcnow().shift(hours=-1)
|
||||||
})
|
})
|
||||||
assert msg_mock.call_args[0][0] \
|
assert msg_mock.call_args[0][0] == ('\N{LARGE BLUE CIRCLE} *Bittrex:* Buying ETH/BTC\n'
|
||||||
== '*Bittrex:* Buying ETH/BTC\n' \
|
'*Amount:* `1333.33333333`\n'
|
||||||
'*Amount:* `1333.33333333`\n' \
|
'*Open Rate:* `0.00001099`\n'
|
||||||
'*Open Rate:* `0.00001099`\n' \
|
'*Current Rate:* `0.00001099`\n'
|
||||||
'*Current Rate:* `0.00001099`\n' \
|
'*Total:* `(0.001000 BTC)`')
|
||||||
'*Total:* `(0.001000 BTC)`'
|
|
||||||
|
|
||||||
|
|
||||||
def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None:
|
def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None:
|
||||||
@ -1473,15 +1476,37 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None:
|
|||||||
'open_date': arrow.utcnow().shift(hours=-2, minutes=-35, seconds=-3),
|
'open_date': arrow.utcnow().shift(hours=-2, minutes=-35, seconds=-3),
|
||||||
'close_date': arrow.utcnow(),
|
'close_date': arrow.utcnow(),
|
||||||
})
|
})
|
||||||
assert msg_mock.call_args[0][0] \
|
assert msg_mock.call_args[0][0] == ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH\n'
|
||||||
== '*Binance:* Selling KEY/ETH\n' \
|
'*Amount:* `1333.33333333`\n'
|
||||||
'*Amount:* `1333.33333333`\n' \
|
'*Open Rate:* `0.00007500`\n'
|
||||||
'*Open Rate:* `0.00007500`\n' \
|
'*Current Rate:* `0.00003201`\n'
|
||||||
'*Current Rate:* `0.00003201`\n' \
|
'*Close Rate:* `0.00003201`\n'
|
||||||
'*Close Rate:* `0.00003201`\n' \
|
'*Sell Reason:* `stop_loss`\n'
|
||||||
'*Sell Reason:* `stop_loss`\n' \
|
'*Duration:* `2:35:03 (155.1 min)`\n'
|
||||||
'*Duration:* `2:35:03 (155.1 min)`\n' \
|
'*Profit:* `-57.41%`')
|
||||||
'*Profit:* `-57.41%`'
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('msg,expected', [
|
||||||
|
({'profit_percent': 20.1, 'sell_reason': 'roi'}, "\N{ROCKET}"),
|
||||||
|
({'profit_percent': 5.1, 'sell_reason': 'roi'}, "\N{ROCKET}"),
|
||||||
|
({'profit_percent': 2.56, 'sell_reason': 'roi'}, "\N{EIGHT SPOKED ASTERISK}"),
|
||||||
|
({'profit_percent': 1.0, 'sell_reason': 'roi'}, "\N{EIGHT SPOKED ASTERISK}"),
|
||||||
|
({'profit_percent': 0.0, 'sell_reason': 'roi'}, "\N{EIGHT SPOKED ASTERISK}"),
|
||||||
|
({'profit_percent': -5.0, 'sell_reason': 'stop_loss'}, "\N{WARNING SIGN}"),
|
||||||
|
({'profit_percent': -2.0, 'sell_reason': 'sell_signal'}, "\N{CROSS MARK}"),
|
||||||
|
])
|
||||||
|
def test__sell_emoji(default_conf, mocker, msg, expected):
|
||||||
|
del default_conf['fiat_display_currency']
|
||||||
|
msg_mock = MagicMock()
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.rpc.telegram.Telegram',
|
||||||
|
_init=MagicMock(),
|
||||||
|
_send_msg=msg_mock
|
||||||
|
)
|
||||||
|
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
|
assert telegram._get_sell_emoji(msg) == expected
|
||||||
|
|
||||||
|
|
||||||
def test__send_msg(default_conf, mocker) -> None:
|
def test__send_msg(default_conf, mocker) -> None:
|
||||||
|
@ -1126,7 +1126,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, 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.Exchange.get_order', hanging_stoploss_order)
|
mocker.patch('freqtrade.exchange.Exchange.get_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
|
||||||
@ -1139,7 +1139,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, 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.Exchange.get_order', canceled_stoploss_order)
|
mocker.patch('freqtrade.exchange.Exchange.get_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
|
||||||
@ -1164,7 +1164,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
|
|||||||
'average': 2,
|
'average': 2,
|
||||||
'amount': limit_buy_order['amount'],
|
'amount': limit_buy_order['amount'],
|
||||||
})
|
})
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_order_hit)
|
mocker.patch('freqtrade.exchange.Exchange.get_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('STOP_LOSS_LIMIT is hit for {}.'.format(trade), caplog)
|
assert log_has('STOP_LOSS_LIMIT is hit for {}.'.format(trade), caplog)
|
||||||
assert trade.stoploss_order_id is None
|
assert trade.stoploss_order_id is None
|
||||||
@ -1183,7 +1183,8 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, 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.Exchange.get_order', side_effect=InvalidOrderException())
|
mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order',
|
||||||
|
side_effect=InvalidOrderException())
|
||||||
mocker.patch('freqtrade.exchange.Exchange.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
|
||||||
@ -1214,7 +1215,7 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf, fee, caplog,
|
|||||||
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
||||||
sell=MagicMock(return_value={'id': limit_sell_order['id']}),
|
sell=MagicMock(return_value={'id': limit_sell_order['id']}),
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
get_order=MagicMock(return_value={'status': 'canceled'}),
|
get_stoploss_order=MagicMock(return_value={'status': 'canceled'}),
|
||||||
stoploss=MagicMock(side_effect=DependencyException()),
|
stoploss=MagicMock(side_effect=DependencyException()),
|
||||||
)
|
)
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
@ -1331,7 +1332,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog,
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_order_hanging)
|
mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', stoploss_order_hanging)
|
||||||
|
|
||||||
# stoploss initially at 5%
|
# stoploss initially at 5%
|
||||||
assert freqtrade.handle_trade(trade) is False
|
assert freqtrade.handle_trade(trade) is False
|
||||||
@ -1346,7 +1347,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog,
|
|||||||
|
|
||||||
cancel_order_mock = MagicMock()
|
cancel_order_mock = MagicMock()
|
||||||
stoploss_order_mock = MagicMock()
|
stoploss_order_mock = MagicMock()
|
||||||
mocker.patch('freqtrade.exchange.Exchange.cancel_order', cancel_order_mock)
|
mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', cancel_order_mock)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss_order_mock)
|
mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss_order_mock)
|
||||||
|
|
||||||
# stoploss should not be updated as the interval is 60 seconds
|
# stoploss should not be updated as the interval is 60 seconds
|
||||||
@ -1429,8 +1430,9 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c
|
|||||||
'stopPrice': '0.1'
|
'stopPrice': '0.1'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mocker.patch('freqtrade.exchange.Exchange.cancel_order', side_effect=InvalidOrderException())
|
mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order',
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_order_hanging)
|
side_effect=InvalidOrderException())
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', stoploss_order_hanging)
|
||||||
freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging)
|
freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging)
|
||||||
assert log_has_re(r"Could not cancel stoploss order abcd for pair ETH/BTC.*", caplog)
|
assert log_has_re(r"Could not cancel stoploss order abcd for pair ETH/BTC.*", caplog)
|
||||||
|
|
||||||
@ -1439,7 +1441,7 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c
|
|||||||
|
|
||||||
# Fail creating stoploss order
|
# Fail creating stoploss order
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
cancel_mock = mocker.patch("freqtrade.exchange.Exchange.cancel_order", MagicMock())
|
cancel_mock = mocker.patch("freqtrade.exchange.Exchange.cancel_stoploss_order", MagicMock())
|
||||||
mocker.patch("freqtrade.exchange.Exchange.stoploss", side_effect=DependencyException())
|
mocker.patch("freqtrade.exchange.Exchange.stoploss", side_effect=DependencyException())
|
||||||
freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging)
|
freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging)
|
||||||
assert cancel_mock.call_count == 1
|
assert cancel_mock.call_count == 1
|
||||||
@ -1510,7 +1512,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog,
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_order_hanging)
|
mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', stoploss_order_hanging)
|
||||||
|
|
||||||
# stoploss initially at 20% as edge dictated it.
|
# stoploss initially at 20% as edge dictated it.
|
||||||
assert freqtrade.handle_trade(trade) is False
|
assert freqtrade.handle_trade(trade) is False
|
||||||
@ -1519,7 +1521,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog,
|
|||||||
|
|
||||||
cancel_order_mock = MagicMock()
|
cancel_order_mock = MagicMock()
|
||||||
stoploss_order_mock = MagicMock()
|
stoploss_order_mock = MagicMock()
|
||||||
mocker.patch('freqtrade.exchange.Exchange.cancel_order', cancel_order_mock)
|
mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', cancel_order_mock)
|
||||||
mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss_order_mock)
|
mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss_order_mock)
|
||||||
|
|
||||||
# price goes down 5%
|
# price goes down 5%
|
||||||
@ -2632,7 +2634,8 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe
|
|||||||
|
|
||||||
def test_execute_sell_sloe_cancel_exception(mocker, default_conf, ticker, fee, caplog) -> None:
|
def test_execute_sell_sloe_cancel_exception(mocker, default_conf, ticker, fee, caplog) -> None:
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.cancel_order', side_effect=InvalidOrderException())
|
mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order',
|
||||||
|
side_effect=InvalidOrderException())
|
||||||
mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=300))
|
mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=300))
|
||||||
sellmock = MagicMock()
|
sellmock = MagicMock()
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
@ -2680,7 +2683,7 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, ticker, fee, ticke
|
|||||||
amount_to_precision=lambda s, x, y: y,
|
amount_to_precision=lambda s, x, y: y,
|
||||||
price_to_precision=lambda s, x, y: y,
|
price_to_precision=lambda s, x, y: y,
|
||||||
stoploss=stoploss,
|
stoploss=stoploss,
|
||||||
cancel_order=cancel_order,
|
cancel_stoploss_order=cancel_order,
|
||||||
)
|
)
|
||||||
|
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
@ -2771,7 +2774,7 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, ticker, f
|
|||||||
"fee": None,
|
"fee": None,
|
||||||
"trades": None
|
"trades": None
|
||||||
})
|
})
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_executed)
|
mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', stoploss_executed)
|
||||||
|
|
||||||
freqtrade.exit_positions(trades)
|
freqtrade.exit_positions(trades)
|
||||||
assert trade.stoploss_order_id is None
|
assert trade.stoploss_order_id is None
|
||||||
|
@ -62,8 +62,8 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee,
|
|||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
amount_to_precision=lambda s, x, y: y,
|
amount_to_precision=lambda s, x, y: y,
|
||||||
price_to_precision=lambda s, x, y: y,
|
price_to_precision=lambda s, x, y: y,
|
||||||
get_order=stoploss_order_mock,
|
get_stoploss_order=stoploss_order_mock,
|
||||||
cancel_order=cancel_order_mock,
|
cancel_stoploss_order=cancel_order_mock,
|
||||||
)
|
)
|
||||||
|
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
|
@ -141,12 +141,12 @@ def test_main_operational_exception1(mocker, default_conf, caplog) -> None:
|
|||||||
assert log_has_re(r'SIGINT.*', caplog)
|
assert log_has_re(r'SIGINT.*', caplog)
|
||||||
|
|
||||||
|
|
||||||
def test_main_reload_conf(mocker, default_conf, caplog) -> None:
|
def test_main_reload_config(mocker, default_conf, caplog) -> None:
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.cleanup', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.cleanup', MagicMock())
|
||||||
# Simulate Running, reload, running workflow
|
# Simulate Running, reload, running workflow
|
||||||
worker_mock = MagicMock(side_effect=[State.RUNNING,
|
worker_mock = MagicMock(side_effect=[State.RUNNING,
|
||||||
State.RELOAD_CONF,
|
State.RELOAD_CONFIG,
|
||||||
State.RUNNING,
|
State.RUNNING,
|
||||||
OperationalException("Oh snap!")])
|
OperationalException("Oh snap!")])
|
||||||
mocker.patch('freqtrade.worker.Worker._worker', worker_mock)
|
mocker.patch('freqtrade.worker.Worker._worker', worker_mock)
|
||||||
|
@ -298,7 +298,7 @@ def test_calc_profit(limit_buy_order, limit_sell_order, fee):
|
|||||||
fee_close=fee.return_value,
|
fee_close=fee.return_value,
|
||||||
exchange='bittrex',
|
exchange='bittrex',
|
||||||
)
|
)
|
||||||
trade.open_order_id = 'profit_percent'
|
trade.open_order_id = 'something'
|
||||||
trade.update(limit_buy_order) # Buy @ 0.00001099
|
trade.update(limit_buy_order) # Buy @ 0.00001099
|
||||||
|
|
||||||
# Custom closing rate and regular fee rate
|
# Custom closing rate and regular fee rate
|
||||||
@ -332,7 +332,7 @@ def test_calc_profit_ratio(limit_buy_order, limit_sell_order, fee):
|
|||||||
fee_close=fee.return_value,
|
fee_close=fee.return_value,
|
||||||
exchange='bittrex',
|
exchange='bittrex',
|
||||||
)
|
)
|
||||||
trade.open_order_id = 'profit_percent'
|
trade.open_order_id = 'something'
|
||||||
trade.update(limit_buy_order) # Buy @ 0.00001099
|
trade.update(limit_buy_order) # Buy @ 0.00001099
|
||||||
|
|
||||||
# Get percent of profit with a custom rate (Higher than open rate)
|
# Get percent of profit with a custom rate (Higher than open rate)
|
||||||
|
@ -124,7 +124,7 @@ def test_plot_trades(testdatadir, caplog):
|
|||||||
trade_sell = find_trace_in_fig_data(figure.data, 'Sell - Profit')
|
trade_sell = find_trace_in_fig_data(figure.data, 'Sell - Profit')
|
||||||
assert isinstance(trade_sell, go.Scatter)
|
assert isinstance(trade_sell, go.Scatter)
|
||||||
assert trade_sell.yaxis == 'y'
|
assert trade_sell.yaxis == 'y'
|
||||||
assert len(trades.loc[trades['profitperc'] > 0]) == len(trade_sell.x)
|
assert len(trades.loc[trades['profit_percent'] > 0]) == len(trade_sell.x)
|
||||||
assert trade_sell.marker.color == 'green'
|
assert trade_sell.marker.color == 'green'
|
||||||
assert trade_sell.marker.symbol == 'square-open'
|
assert trade_sell.marker.symbol == 'square-open'
|
||||||
assert trade_sell.text[0] == '4.0%, roi, 15 min'
|
assert trade_sell.text[0] == '4.0%, roi, 15 min'
|
||||||
@ -132,7 +132,7 @@ def test_plot_trades(testdatadir, caplog):
|
|||||||
trade_sell_loss = find_trace_in_fig_data(figure.data, 'Sell - Loss')
|
trade_sell_loss = find_trace_in_fig_data(figure.data, 'Sell - Loss')
|
||||||
assert isinstance(trade_sell_loss, go.Scatter)
|
assert isinstance(trade_sell_loss, go.Scatter)
|
||||||
assert trade_sell_loss.yaxis == 'y'
|
assert trade_sell_loss.yaxis == 'y'
|
||||||
assert len(trades.loc[trades['profitperc'] <= 0]) == len(trade_sell_loss.x)
|
assert len(trades.loc[trades['profit_percent'] <= 0]) == len(trade_sell_loss.x)
|
||||||
assert trade_sell_loss.marker.color == 'red'
|
assert trade_sell_loss.marker.color == 'red'
|
||||||
assert trade_sell_loss.marker.symbol == 'square-open'
|
assert trade_sell_loss.marker.symbol == 'square-open'
|
||||||
assert trade_sell_loss.text[5] == '-10.4%, stop_loss, 720 min'
|
assert trade_sell_loss.text[5] == '-10.4%, stop_loss, 720 min'
|
||||||
|
Loading…
Reference in New Issue
Block a user