Merge branch 'develop' into pr/mkavinkumar1/6545
This commit is contained in:
commit
060f12572e
28
.github/workflows/ci.yml
vendored
28
.github/workflows/ci.yml
vendored
@ -13,6 +13,10 @@ on:
|
||||
schedule:
|
||||
- cron: '0 5 * * 4'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build_linux:
|
||||
|
||||
@ -26,7 +30,7 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v3
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
@ -123,7 +127,7 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v3
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
@ -207,7 +211,7 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v3
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
@ -259,7 +263,7 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v3
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.10"
|
||||
|
||||
@ -278,7 +282,7 @@ jobs:
|
||||
./tests/test_docs.sh
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v3
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.10"
|
||||
|
||||
@ -296,18 +300,6 @@ jobs:
|
||||
details: Freqtrade doc test failed!
|
||||
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
|
||||
cleanup-prior-runs:
|
||||
permissions:
|
||||
actions: write # for rokroskar/workflow-run-cleanup-action to obtain workflow name & cancel it
|
||||
contents: read # for rokroskar/workflow-run-cleanup-action to obtain branch
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Cleanup previous runs on this branch
|
||||
uses: rokroskar/workflow-run-cleanup-action@v0.3.3
|
||||
if: "!startsWith(github.ref, 'refs/tags/') && github.ref != 'refs/heads/stable' && github.repository == 'freqtrade/freqtrade'"
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
|
||||
# Notify only once - when CI completes (and after deploy) in case it's successfull
|
||||
notify-complete:
|
||||
needs: [ build_linux, build_macos, build_windows, docs_check, mypy_version_check ]
|
||||
@ -344,7 +336,7 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v3
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.9"
|
||||
|
||||
|
@ -14,10 +14,10 @@ repos:
|
||||
exclude: build_helpers
|
||||
additional_dependencies:
|
||||
- types-cachetools==5.0.1
|
||||
- types-filelock==3.2.6
|
||||
- types-requests==2.27.27
|
||||
- types-filelock==3.2.7
|
||||
- types-requests==2.27.30
|
||||
- types-tabulate==0.8.9
|
||||
- types-python-dateutil==2.8.16
|
||||
- types-python-dateutil==2.8.17
|
||||
# stages: [push]
|
||||
|
||||
- repo: https://github.com/pycqa/isort
|
||||
|
@ -1,4 +1,4 @@
|
||||
FROM python:3.10.4-slim-bullseye as base
|
||||
FROM python:3.10.5-slim-bullseye as base
|
||||
|
||||
# Setup env
|
||||
ENV LANG C.UTF-8
|
||||
|
@ -98,6 +98,23 @@ class MyAwesomeStrategy(IStrategy):
|
||||
!!! Note
|
||||
All overrides are optional and can be mixed/matched as necessary.
|
||||
|
||||
### Dynamic parameters
|
||||
|
||||
Parameters can also be defined dynamically, but must be available to the instance once the * [`bot_start()` callback](strategy-callbacks.md#bot-start) has been called.
|
||||
|
||||
``` python
|
||||
|
||||
class MyAwesomeStrategy(IStrategy):
|
||||
|
||||
def bot_start(self, **kwargs) -> None:
|
||||
self.buy_adx = IntParameter(20, 30, default=30, optimize=True)
|
||||
|
||||
# ...
|
||||
```
|
||||
|
||||
!!! Warning
|
||||
Parameters created this way will not show up in the `list-strategies` parameter count.
|
||||
|
||||
### Overriding Base estimator
|
||||
|
||||
You can define your own estimator for Hyperopt by implementing `generate_estimator()` in the Hyperopt subclass.
|
||||
|
BIN
docs/assets/discord_notification.png
Normal file
BIN
docs/assets/discord_notification.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 48 KiB |
@ -680,7 +680,7 @@ class MyAwesomeStrategy(IStrategy):
|
||||
|
||||
!!! Note
|
||||
Values in the configuration file will overwrite Parameter-file level parameters - and both will overwrite parameters within the strategy.
|
||||
The prevalence is therefore: config > parameter file > strategy
|
||||
The prevalence is therefore: config > parameter file > strategy `*_params` > parameter default
|
||||
|
||||
### Understand Hyperopt ROI results
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
mkdocs==1.3.0
|
||||
mkdocs-material==8.2.15
|
||||
mkdocs-material==8.3.4
|
||||
mdx_truly_sane_lists==1.2
|
||||
pymdown-extensions==9.4
|
||||
pymdown-extensions==9.5
|
||||
jinja2==3.1.2
|
||||
|
@ -89,11 +89,12 @@ WHERE id=31;
|
||||
|
||||
If you'd still like to remove a trade from the database directly, you can use the below query.
|
||||
|
||||
```sql
|
||||
DELETE FROM trades WHERE id = <tradeid>;
|
||||
```
|
||||
!!! Danger
|
||||
Some systems (Ubuntu) disable foreign keys in their sqlite3 packaging. When using sqlite - please ensure that foreign keys are on by running `PRAGMA foreign_keys = ON` before the above query.
|
||||
|
||||
```sql
|
||||
DELETE FROM trades WHERE id = <tradeid>;
|
||||
|
||||
DELETE FROM trades WHERE id = 31;
|
||||
```
|
||||
|
||||
@ -102,13 +103,20 @@ DELETE FROM trades WHERE id = 31;
|
||||
|
||||
## Use a different database system
|
||||
|
||||
Freqtrade is using SQLAlchemy, which supports multiple different database systems. As such, a multitude of database systems should be supported.
|
||||
Freqtrade does not depend or install any additional database driver. Please refer to the [SQLAlchemy docs](https://docs.sqlalchemy.org/en/14/core/engines.html#database-urls) on installation instructions for the respective database systems.
|
||||
|
||||
The following systems have been tested and are known to work with freqtrade:
|
||||
|
||||
* sqlite (default)
|
||||
* PostgreSQL)
|
||||
* MariaDB
|
||||
|
||||
!!! Warning
|
||||
By using one of the below database systems, you acknowledge that you know how to manage such a system. Freqtrade will not provide any support with setup or maintenance (or backups) of the below database systems.
|
||||
By using one of the below database systems, you acknowledge that you know how to manage such a system. The freqtrade team will not provide any support with setup or maintenance (or backups) of the below database systems.
|
||||
|
||||
### PostgreSQL
|
||||
|
||||
Freqtrade supports PostgreSQL by using SQLAlchemy, which supports multiple different database systems.
|
||||
|
||||
Installation:
|
||||
`pip install psycopg2-binary`
|
||||
|
||||
|
@ -46,6 +46,9 @@ class AwesomeStrategy(IStrategy):
|
||||
self.cust_remote_data = requests.get('https://some_remote_source.example.com')
|
||||
|
||||
```
|
||||
|
||||
During hyperopt, this runs only once at startup.
|
||||
|
||||
## Bot loop start
|
||||
|
||||
A simple callback which is called once at the start of every bot throttling iteration (roughly every 5 seconds, unless configured differently).
|
||||
@ -546,10 +549,12 @@ class AwesomeStrategy(IStrategy):
|
||||
|
||||
:param pair: Pair that's about to be bought/shorted.
|
||||
:param order_type: Order type (as configured in order_types). usually limit or market.
|
||||
:param amount: Amount in target (quote) currency that's going to be traded.
|
||||
:param amount: Amount in target (base) currency that's going to be traded.
|
||||
:param rate: Rate that's going to be used when using limit orders
|
||||
or current rate for market orders.
|
||||
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
|
||||
:param current_time: datetime object, containing the current datetime
|
||||
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
|
||||
:param side: 'long' or 'short' - indicating the direction of the proposed trade
|
||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||
:return bool: When True is returned, then the buy-order is placed on the exchange.
|
||||
@ -583,7 +588,7 @@ class AwesomeStrategy(IStrategy):
|
||||
rate: float, time_in_force: str, exit_reason: str,
|
||||
current_time: datetime, **kwargs) -> bool:
|
||||
"""
|
||||
Called right before placing a regular sell order.
|
||||
Called right before placing a regular exit order.
|
||||
Timing for this function is critical, so avoid doing heavy computations or
|
||||
network requests in this method.
|
||||
|
||||
@ -591,17 +596,19 @@ class AwesomeStrategy(IStrategy):
|
||||
|
||||
When not implemented by a strategy, returns True (always confirming).
|
||||
|
||||
:param pair: Pair that's about to be sold.
|
||||
:param pair: Pair for trade that's about to be exited.
|
||||
:param trade: trade object.
|
||||
:param order_type: Order type (as configured in order_types). usually limit or market.
|
||||
:param amount: Amount in quote currency.
|
||||
:param amount: Amount in base currency.
|
||||
:param rate: Rate that's going to be used when using limit orders
|
||||
or current rate for market orders.
|
||||
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
|
||||
:param exit_reason: Exit reason.
|
||||
Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss',
|
||||
'exit_signal', 'force_exit', 'emergency_exit']
|
||||
:param current_time: datetime object, containing the current datetime
|
||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||
:return bool: When True is returned, then the exit-order is placed on the exchange.
|
||||
:return bool: When True, then the exit-order is placed on the exchange.
|
||||
False aborts the process
|
||||
"""
|
||||
if exit_reason == 'force_exit' and trade.calc_profit_ratio(rate) < 0:
|
||||
@ -817,17 +824,18 @@ For markets / exchanges that don't support leverage, this method is ignored.
|
||||
|
||||
``` python
|
||||
class AwesomeStrategy(IStrategy):
|
||||
def leverage(self, pair: str, current_time: 'datetime', current_rate: float,
|
||||
proposed_leverage: float, max_leverage: float, side: str,
|
||||
def leverage(self, pair: str, current_time: datetime, current_rate: float,
|
||||
proposed_leverage: float, max_leverage: float, entry_tag: Optional[str], side: str,
|
||||
**kwargs) -> float:
|
||||
"""
|
||||
Customize leverage for each new trade.
|
||||
Customize leverage for each new trade. This method is only called in futures mode.
|
||||
|
||||
:param pair: Pair that's currently analyzed
|
||||
:param current_time: datetime object, containing the current datetime
|
||||
:param current_rate: Rate, calculated based on pricing settings in exit_pricing.
|
||||
:param proposed_leverage: A leverage proposed by the bot.
|
||||
:param max_leverage: Max leverage allowed on this pair
|
||||
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
|
||||
:param side: 'long' or 'short' - indicating the direction of the proposed trade
|
||||
:return: A leverage amount, which is between 1.0 and max_leverage.
|
||||
"""
|
||||
|
@ -328,11 +328,11 @@ Per default `/daily` will return the 7 last days. The example below if for `/dai
|
||||
|
||||
> **Daily Profit over the last 3 days:**
|
||||
```
|
||||
Day Profit BTC Profit USD
|
||||
---------- -------------- ------------
|
||||
2018-01-03 0.00224175 BTC 29,142 USD
|
||||
2018-01-02 0.00033131 BTC 4,307 USD
|
||||
2018-01-01 0.00269130 BTC 34.986 USD
|
||||
Day (count) USDT USD Profit %
|
||||
-------------- ------------ ---------- ----------
|
||||
2022-06-11 (1) -0.746 USDT -0.75 USD -0.08%
|
||||
2022-06-10 (0) 0 USDT 0.00 USD 0.00%
|
||||
2022-06-09 (5) 20 USDT 20.10 USD 5.00%
|
||||
```
|
||||
|
||||
### /weekly <n>
|
||||
@ -342,11 +342,11 @@ from Monday. The example below if for `/weekly 3`:
|
||||
|
||||
> **Weekly Profit over the last 3 weeks (starting from Monday):**
|
||||
```
|
||||
Monday Profit BTC Profit USD
|
||||
---------- -------------- ------------
|
||||
2018-01-03 0.00224175 BTC 29,142 USD
|
||||
2017-12-27 0.00033131 BTC 4,307 USD
|
||||
2017-12-20 0.00269130 BTC 34.986 USD
|
||||
Monday (count) Profit BTC Profit USD Profit %
|
||||
------------- -------------- ------------ ----------
|
||||
2018-01-03 (5) 0.00224175 BTC 29,142 USD 4.98%
|
||||
2017-12-27 (1) 0.00033131 BTC 4,307 USD 0.00%
|
||||
2017-12-20 (4) 0.00269130 BTC 34.986 USD 5.12%
|
||||
```
|
||||
|
||||
### /monthly <n>
|
||||
@ -356,11 +356,11 @@ if for `/monthly 3`:
|
||||
|
||||
> **Monthly Profit over the last 3 months:**
|
||||
```
|
||||
Month Profit BTC Profit USD
|
||||
---------- -------------- ------------
|
||||
2018-01 0.00224175 BTC 29,142 USD
|
||||
2017-12 0.00033131 BTC 4,307 USD
|
||||
2017-11 0.00269130 BTC 34.986 USD
|
||||
Month (count) Profit BTC Profit USD Profit %
|
||||
------------- -------------- ------------ ----------
|
||||
2018-01 (20) 0.00224175 BTC 29,142 USD 4.98%
|
||||
2017-12 (5) 0.00033131 BTC 4,307 USD 0.00%
|
||||
2017-11 (10) 0.00269130 BTC 34.986 USD 5.10%
|
||||
```
|
||||
|
||||
### /whitelist
|
||||
|
@ -32,4 +32,8 @@ Please ensure that you're also updating dependencies - otherwise things might br
|
||||
``` bash
|
||||
git pull
|
||||
pip install -U -r requirements.txt
|
||||
pip install -e .
|
||||
|
||||
# Ensure freqUI is at the latest version
|
||||
freqtrade install-ui
|
||||
```
|
||||
|
@ -239,3 +239,52 @@ Possible parameters are:
|
||||
The fields in `webhook.webhookstatus` are used for regular status messages (Started / Stopped / ...). Parameters are filled using string.format.
|
||||
|
||||
The only possible value here is `{status}`.
|
||||
|
||||
## Discord
|
||||
|
||||
A special form of webhooks is available for discord.
|
||||
You can configure this as follows:
|
||||
|
||||
```json
|
||||
"discord": {
|
||||
"enabled": true,
|
||||
"webhook_url": "https://discord.com/api/webhooks/<Your webhook URL ...>",
|
||||
"exit_fill": [
|
||||
{"Trade ID": "{trade_id}"},
|
||||
{"Exchange": "{exchange}"},
|
||||
{"Pair": "{pair}"},
|
||||
{"Direction": "{direction}"},
|
||||
{"Open rate": "{open_rate}"},
|
||||
{"Close rate": "{close_rate}"},
|
||||
{"Amount": "{amount}"},
|
||||
{"Open date": "{open_date:%Y-%m-%d %H:%M:%S}"},
|
||||
{"Close date": "{close_date:%Y-%m-%d %H:%M:%S}"},
|
||||
{"Profit": "{profit_amount} {stake_currency}"},
|
||||
{"Profitability": "{profit_ratio:.2%}"},
|
||||
{"Enter tag": "{enter_tag}"},
|
||||
{"Exit Reason": "{exit_reason}"},
|
||||
{"Strategy": "{strategy}"},
|
||||
{"Timeframe": "{timeframe}"},
|
||||
],
|
||||
"entry_fill": [
|
||||
{"Trade ID": "{trade_id}"},
|
||||
{"Exchange": "{exchange}"},
|
||||
{"Pair": "{pair}"},
|
||||
{"Direction": "{direction}"},
|
||||
{"Open rate": "{open_rate}"},
|
||||
{"Amount": "{amount}"},
|
||||
{"Open date": "{open_date:%Y-%m-%d %H:%M:%S}"},
|
||||
{"Enter tag": "{enter_tag}"},
|
||||
{"Strategy": "{strategy} {timeframe}"},
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
The above represents the default (`exit_fill` and `entry_fill` are optional and will default to the above configuration) - modifications are obviously possible.
|
||||
|
||||
Available fields correspond to the fields for webhooks and are documented in the corresponding webhook sections.
|
||||
|
||||
The notifications will look as follows by default.
|
||||
|
||||

|
||||
|
@ -336,6 +336,47 @@ CONF_SCHEMA = {
|
||||
'webhookstatus': {'type': 'object'},
|
||||
},
|
||||
},
|
||||
'discord': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'enabled': {'type': 'boolean'},
|
||||
'webhook_url': {'type': 'string'},
|
||||
"exit_fill": {
|
||||
'type': 'array', 'items': {'type': 'object'},
|
||||
'default': [
|
||||
{"Trade ID": "{trade_id}"},
|
||||
{"Exchange": "{exchange}"},
|
||||
{"Pair": "{pair}"},
|
||||
{"Direction": "{direction}"},
|
||||
{"Open rate": "{open_rate}"},
|
||||
{"Close rate": "{close_rate}"},
|
||||
{"Amount": "{amount}"},
|
||||
{"Open date": "{open_date:%Y-%m-%d %H:%M:%S}"},
|
||||
{"Close date": "{close_date:%Y-%m-%d %H:%M:%S}"},
|
||||
{"Profit": "{profit_amount} {stake_currency}"},
|
||||
{"Profitability": "{profit_ratio:.2%}"},
|
||||
{"Enter tag": "{enter_tag}"},
|
||||
{"Exit Reason": "{exit_reason}"},
|
||||
{"Strategy": "{strategy}"},
|
||||
{"Timeframe": "{timeframe}"},
|
||||
]
|
||||
},
|
||||
"entry_fill": {
|
||||
'type': 'array', 'items': {'type': 'object'},
|
||||
'default': [
|
||||
{"Trade ID": "{trade_id}"},
|
||||
{"Exchange": "{exchange}"},
|
||||
{"Pair": "{pair}"},
|
||||
{"Direction": "{direction}"},
|
||||
{"Open rate": "{open_rate}"},
|
||||
{"Amount": "{amount}"},
|
||||
{"Open date": "{open_date:%Y-%m-%d %H:%M:%S}"},
|
||||
{"Enter tag": "{enter_tag}"},
|
||||
{"Strategy": "{strategy} {timeframe}"},
|
||||
]
|
||||
},
|
||||
}
|
||||
},
|
||||
'api_server': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
|
@ -26,7 +26,7 @@ BT_DATA_COLUMNS = ['pair', 'stake_amount', 'amount', 'open_date', 'close_date',
|
||||
'profit_ratio', 'profit_abs', 'exit_reason',
|
||||
'initial_stop_loss_abs', 'initial_stop_loss_ratio', 'stop_loss_abs',
|
||||
'stop_loss_ratio', 'min_rate', 'max_rate', 'is_open', 'enter_tag',
|
||||
'is_short'
|
||||
'is_short', 'open_timestamp', 'close_timestamp', 'orders'
|
||||
]
|
||||
|
||||
|
||||
@ -283,6 +283,8 @@ def load_backtest_data(filename: Union[Path, str], strategy: Optional[str] = Non
|
||||
if 'enter_tag' not in df.columns:
|
||||
df['enter_tag'] = df['buy_tag']
|
||||
df = df.drop(['buy_tag'], axis=1)
|
||||
if 'orders' not in df.columns:
|
||||
df.loc[:, 'orders'] = None
|
||||
|
||||
else:
|
||||
# old format - only with lists.
|
||||
@ -337,7 +339,7 @@ def trade_list_to_dataframe(trades: List[LocalTrade]) -> pd.DataFrame:
|
||||
:param trades: List of trade objects
|
||||
:return: Dataframe with BT_DATA_COLUMNS
|
||||
"""
|
||||
df = pd.DataFrame.from_records([t.to_json() for t in trades], columns=BT_DATA_COLUMNS)
|
||||
df = pd.DataFrame.from_records([t.to_json(True) for t in trades], columns=BT_DATA_COLUMNS)
|
||||
if len(df) > 0:
|
||||
df.loc[:, 'close_date'] = pd.to_datetime(df['close_date'], utc=True)
|
||||
df.loc[:, 'open_date'] = pd.to_datetime(df['open_date'], utc=True)
|
||||
|
@ -53,8 +53,8 @@ class Binance(Exchange):
|
||||
ordertype = 'stop' if self.trading_mode == TradingMode.FUTURES else 'stop_loss_limit'
|
||||
|
||||
return order['type'] == ordertype and (
|
||||
(side == "sell" and stop_loss > float(order['info']['stopPrice'])) or
|
||||
(side == "buy" and stop_loss < float(order['info']['stopPrice']))
|
||||
(side == "sell" and stop_loss > float(order['stopPrice'])) or
|
||||
(side == "buy" and stop_loss < float(order['stopPrice']))
|
||||
)
|
||||
|
||||
def get_tickers(self, symbols: Optional[List[str]] = None, cached: bool = False) -> Dict:
|
||||
|
@ -4,7 +4,7 @@ Freqtrade is the main module of this bot. It contains the class Freqtrade()
|
||||
import copy
|
||||
import logging
|
||||
import traceback
|
||||
from datetime import datetime, time, timezone
|
||||
from datetime import datetime, time, timedelta, timezone
|
||||
from decimal import Decimal
|
||||
from math import isclose
|
||||
from threading import Lock
|
||||
@ -74,8 +74,6 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
PairLocks.timeframe = self.config['timeframe']
|
||||
|
||||
self.protections = ProtectionManager(self.config, self.strategy.protections)
|
||||
|
||||
# RPC runs in separate threads, can start handling external commands just after
|
||||
# initialization, even before Freqtradebot has a chance to start its throttling,
|
||||
# so anything in the Freqtradebot instance should be ready (initialized), including
|
||||
@ -125,6 +123,8 @@ class FreqtradeBot(LoggingMixin):
|
||||
self.last_process = datetime(1970, 1, 1, tzinfo=timezone.utc)
|
||||
|
||||
self.strategy.ft_bot_start()
|
||||
# Initialize protections AFTER bot start - otherwise parameters are not loaded.
|
||||
self.protections = ProtectionManager(self.config, self.strategy.protections)
|
||||
|
||||
def notify_status(self, msg: str) -> None:
|
||||
"""
|
||||
@ -228,7 +228,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
Notify the user when the bot is stopped (not reloaded)
|
||||
and there are still open trades active.
|
||||
"""
|
||||
open_trades = Trade.get_trades([Trade.is_open.is_(True)]).all()
|
||||
open_trades = Trade.get_open_trades()
|
||||
|
||||
if len(open_trades) != 0 and self.state != State.RELOAD_CONFIG:
|
||||
msg = {
|
||||
@ -303,6 +303,15 @@ class FreqtradeBot(LoggingMixin):
|
||||
self.update_trade_state(order.trade, order.order_id, fo,
|
||||
stoploss_order=(order.ft_order_side == 'stoploss'))
|
||||
|
||||
except InvalidOrderException as e:
|
||||
logger.warning(f"Error updating Order {order.order_id} due to {e}.")
|
||||
if order.order_date_utc - timedelta(days=5) < datetime.now(timezone.utc):
|
||||
logger.warning(
|
||||
"Order is older than 5 days. Assuming order was fully cancelled.")
|
||||
fo = order.to_ccxt_object()
|
||||
fo['status'] = 'canceled'
|
||||
self.handle_timedout_order(fo, order.trade)
|
||||
|
||||
except ExchangeError as e:
|
||||
|
||||
logger.warning(f"Error updating Order {order.order_id} due to {e}")
|
||||
@ -803,7 +812,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
current_rate=enter_limit_requested,
|
||||
proposed_leverage=1.0,
|
||||
max_leverage=max_leverage,
|
||||
side=trade_side,
|
||||
side=trade_side, entry_tag=entry_tag,
|
||||
) if self.trading_mode != TradingMode.SPOT else 1.0
|
||||
# Cap leverage between 1.0 and max_leverage.
|
||||
leverage = min(max(leverage, 1.0), max_leverage)
|
||||
@ -1229,15 +1238,15 @@ class FreqtradeBot(LoggingMixin):
|
||||
current_order_rate=order_obj.price, entry_tag=trade.enter_tag,
|
||||
side=trade.entry_side)
|
||||
|
||||
full_cancel = False
|
||||
replacing = True
|
||||
cancel_reason = constants.CANCEL_REASON['REPLACE']
|
||||
if not adjusted_entry_price:
|
||||
full_cancel = True if trade.nr_of_successful_entries == 0 else False
|
||||
replacing = False
|
||||
cancel_reason = constants.CANCEL_REASON['USER_CANCEL']
|
||||
if order_obj.price != adjusted_entry_price:
|
||||
# cancel existing order if new price is supplied or None
|
||||
self.handle_cancel_enter(trade, order, cancel_reason,
|
||||
allow_full_cancel=full_cancel)
|
||||
replacing=replacing)
|
||||
if adjusted_entry_price:
|
||||
# place new order only if new price is supplied
|
||||
self.execute_entry(
|
||||
@ -1271,10 +1280,11 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
def handle_cancel_enter(
|
||||
self, trade: Trade, order: Dict, reason: str,
|
||||
allow_full_cancel: Optional[bool] = True
|
||||
replacing: Optional[bool] = False
|
||||
) -> bool:
|
||||
"""
|
||||
Buy cancel - cancel order
|
||||
:param replacing: Replacing order - prevent trade deletion.
|
||||
:return: True if order was fully cancelled
|
||||
"""
|
||||
was_trade_fully_canceled = False
|
||||
@ -1312,7 +1322,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
if isclose(filled_amount, 0.0, abs_tol=constants.MATH_CLOSE_PREC):
|
||||
# if trade is not partially completed and it's the only order, just delete the trade
|
||||
open_order_count = len([order for order in trade.orders if order.status == 'open'])
|
||||
if open_order_count <= 1 and allow_full_cancel:
|
||||
if open_order_count <= 1 and trade.nr_of_successful_entries == 0 and not replacing:
|
||||
logger.info(f'{side} order fully cancelled. Removing {trade} from database.')
|
||||
trade.delete()
|
||||
was_trade_fully_canceled = True
|
||||
@ -1321,7 +1331,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
# FIXME TODO: This could possibly reworked to not duplicate the code 15 lines below.
|
||||
self.update_trade_state(trade, trade.open_order_id, corder)
|
||||
trade.open_order_id = None
|
||||
logger.info(f'Partial {side} order timeout for {trade}.')
|
||||
logger.info(f'{side} Order timeout for {trade}.')
|
||||
else:
|
||||
# if trade is partially complete, edit the stake details for the trade
|
||||
# and close the order
|
||||
|
@ -187,6 +187,7 @@ class Backtesting:
|
||||
# since a "perfect" stoploss-exit is assumed anyway
|
||||
# And the regular "stoploss" function would not apply to that case
|
||||
self.strategy.order_types['stoploss_on_exchange'] = False
|
||||
|
||||
self.strategy.ft_bot_start()
|
||||
|
||||
def _load_protections(self, strategy: IStrategy):
|
||||
@ -732,7 +733,7 @@ class Backtesting:
|
||||
current_rate=row[OPEN_IDX],
|
||||
proposed_leverage=1.0,
|
||||
max_leverage=max_leverage,
|
||||
side=direction,
|
||||
side=direction, entry_tag=entry_tag,
|
||||
) if self._can_short else 1.0
|
||||
# Cap leverage between 1.0 and max_leverage.
|
||||
leverage = min(max(leverage, 1.0), max_leverage)
|
||||
@ -923,26 +924,30 @@ class Backtesting:
|
||||
self.protections.stop_per_pair(pair, current_time, side)
|
||||
self.protections.global_stop(current_time, side)
|
||||
|
||||
def manage_open_orders(self, trade: LocalTrade, current_time, row: Tuple) -> bool:
|
||||
def manage_open_orders(self, trade: LocalTrade, current_time: datetime, row: Tuple) -> bool:
|
||||
"""
|
||||
Check if any open order needs to be cancelled or replaced.
|
||||
Returns True if the trade should be deleted.
|
||||
"""
|
||||
for order in [o for o in trade.orders if o.ft_is_open]:
|
||||
if self.check_order_cancel(trade, order, current_time):
|
||||
oc = self.check_order_cancel(trade, order, current_time)
|
||||
if oc:
|
||||
# delete trade due to order timeout
|
||||
return True
|
||||
elif self.check_order_replace(trade, order, current_time, row):
|
||||
elif oc is None and self.check_order_replace(trade, order, current_time, row):
|
||||
# delete trade due to user request
|
||||
self.canceled_trade_entries += 1
|
||||
return True
|
||||
# default maintain trade
|
||||
return False
|
||||
|
||||
def check_order_cancel(self, trade: LocalTrade, order: Order, current_time) -> bool:
|
||||
def check_order_cancel(
|
||||
self, trade: LocalTrade, order: Order, current_time: datetime) -> Optional[bool]:
|
||||
"""
|
||||
Check if current analyzed order has to be canceled.
|
||||
Returns True if the trade should be Deleted (initial order was canceled).
|
||||
Returns True if the trade should be Deleted (initial order was canceled),
|
||||
False if it's Canceled
|
||||
None if the order is still active.
|
||||
"""
|
||||
timedout = self.strategy.ft_check_timed_out(
|
||||
trade, # type: ignore[arg-type]
|
||||
@ -956,12 +961,15 @@ class Backtesting:
|
||||
else:
|
||||
# Close additional entry order
|
||||
del trade.orders[trade.orders.index(order)]
|
||||
trade.open_order_id = None
|
||||
return False
|
||||
if order.side == trade.exit_side:
|
||||
self.timedout_exit_orders += 1
|
||||
# Close exit order and retry exiting on next signal.
|
||||
del trade.orders[trade.orders.index(order)]
|
||||
|
||||
trade.open_order_id = None
|
||||
return False
|
||||
return None
|
||||
|
||||
def check_order_replace(self, trade: LocalTrade, order: Order, current_time,
|
||||
row: Tuple) -> bool:
|
||||
@ -987,6 +995,7 @@ class Backtesting:
|
||||
return False
|
||||
else:
|
||||
del trade.orders[trade.orders.index(order)]
|
||||
trade.open_order_id = None
|
||||
self.canceled_entry_orders += 1
|
||||
|
||||
# place new order if result was not None
|
||||
@ -1115,6 +1124,7 @@ class Backtesting:
|
||||
# 5. Process exit orders.
|
||||
order = trade.select_order(trade.exit_side, is_open=True)
|
||||
if order and self._get_order_filled(order.price, row):
|
||||
order.close_bt_order(current_time, trade)
|
||||
trade.open_order_id = None
|
||||
sub_trade = order.safe_amount_after_fee != trade.amount
|
||||
if sub_trade:
|
||||
|
@ -429,7 +429,7 @@ class Hyperopt:
|
||||
return new_list
|
||||
i = 0
|
||||
asked_non_tried: List[List[Any]] = []
|
||||
is_random: List[bool] = []
|
||||
is_random_non_tried: List[bool] = []
|
||||
while i < 5 and len(asked_non_tried) < n_points:
|
||||
if i < 3:
|
||||
self.opt.cache_ = {}
|
||||
@ -438,7 +438,7 @@ class Hyperopt:
|
||||
else:
|
||||
asked = unique_list(self.opt.space.rvs(n_samples=n_points * 5))
|
||||
is_random = [True for _ in range(len(asked))]
|
||||
is_random += [rand for x, rand in zip(asked, is_random)
|
||||
is_random_non_tried += [rand for x, rand in zip(asked, is_random)
|
||||
if x not in self.opt.Xi
|
||||
and x not in asked_non_tried]
|
||||
asked_non_tried += [x for x in asked
|
||||
@ -449,7 +449,7 @@ class Hyperopt:
|
||||
if asked_non_tried:
|
||||
return (
|
||||
asked_non_tried[:min(len(asked_non_tried), n_points)],
|
||||
is_random[:min(len(asked_non_tried), n_points)]
|
||||
is_random_non_tried[:min(len(asked_non_tried), n_points)]
|
||||
)
|
||||
else:
|
||||
return self.opt.ask(n_points=n_points), [False for _ in range(n_points)]
|
||||
|
@ -4,7 +4,6 @@ from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Union
|
||||
|
||||
from numpy import int64
|
||||
from pandas import DataFrame, to_datetime
|
||||
from tabulate import tabulate
|
||||
|
||||
@ -417,9 +416,6 @@ def generate_strategy_stats(pairlist: List[str],
|
||||
key=lambda x: x['profit_sum']) if len(pair_results) > 1 else None
|
||||
worst_pair = min([pair for pair in pair_results if pair['key'] != 'TOTAL'],
|
||||
key=lambda x: x['profit_sum']) if len(pair_results) > 1 else None
|
||||
if not results.empty:
|
||||
results['open_timestamp'] = results['open_date'].view(int64) // 1e6
|
||||
results['close_timestamp'] = results['close_date'].view(int64) // 1e6
|
||||
|
||||
backtest_days = (max_date - min_date).days or 1
|
||||
strat_stats = {
|
||||
|
@ -248,6 +248,35 @@ def set_sqlite_to_wal(engine):
|
||||
connection.execute(text("PRAGMA journal_mode=wal"))
|
||||
|
||||
|
||||
def fix_old_dry_orders(engine):
|
||||
with engine.begin() as connection:
|
||||
connection.execute(
|
||||
text(
|
||||
"""
|
||||
update orders
|
||||
set ft_is_open = 0
|
||||
where ft_is_open = 1 and (ft_trade_id, order_id) not in (
|
||||
select id, stoploss_order_id from trades where stoploss_order_id is not null
|
||||
) and ft_order_side = 'stoploss'
|
||||
and order_id like 'dry_%'
|
||||
"""
|
||||
)
|
||||
)
|
||||
connection.execute(
|
||||
text(
|
||||
"""
|
||||
update orders
|
||||
set ft_is_open = 0
|
||||
where ft_is_open = 1
|
||||
and (ft_trade_id, order_id) not in (
|
||||
select id, open_order_id from trades where open_order_id is not null
|
||||
) and ft_order_side != 'stoploss'
|
||||
and order_id like 'dry_%'
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def check_migrate(engine, decl_base, previous_tables) -> None:
|
||||
"""
|
||||
Checks if migration is necessary and migrates if necessary
|
||||
@ -289,3 +318,4 @@ def check_migrate(engine, decl_base, previous_tables) -> None:
|
||||
"start with a fresh database.")
|
||||
|
||||
set_sqlite_to_wal(engine)
|
||||
fix_old_dry_orders(engine)
|
||||
|
@ -76,7 +76,7 @@ class Order(_DECL_BASE):
|
||||
|
||||
@property
|
||||
def safe_filled(self) -> float:
|
||||
return self.filled or self.amount or 0.0
|
||||
return self.filled if self.filled is not None else self.amount or 0.0
|
||||
|
||||
@property
|
||||
def safe_fee_base(self) -> float:
|
||||
@ -139,17 +139,23 @@ class Order(_DECL_BASE):
|
||||
'info': {},
|
||||
}
|
||||
|
||||
def to_json(self, entry_side: str) -> Dict[str, Any]:
|
||||
return {
|
||||
def to_json(self, entry_side: str, minified: bool = False) -> Dict[str, Any]:
|
||||
resp = {
|
||||
'amount': self.amount,
|
||||
'safe_price': self.safe_price,
|
||||
'ft_order_side': self.ft_order_side,
|
||||
'order_filled_timestamp': int(self.order_filled_date.replace(
|
||||
tzinfo=timezone.utc).timestamp() * 1000) if self.order_filled_date else None,
|
||||
'ft_is_entry': self.ft_order_side == entry_side,
|
||||
}
|
||||
if not minified:
|
||||
resp.update({
|
||||
'pair': self.ft_pair,
|
||||
'order_id': self.order_id,
|
||||
'status': self.status,
|
||||
'amount': self.amount,
|
||||
'average': round(self.average, 8) if self.average else 0,
|
||||
'safe_price': self.safe_price,
|
||||
'cost': self.cost if self.cost else 0,
|
||||
'filled': self.filled,
|
||||
'ft_order_side': self.ft_order_side,
|
||||
'is_open': self.ft_is_open,
|
||||
'order_date': self.order_date.strftime(DATETIME_PRINT_FORMAT)
|
||||
if self.order_date else None,
|
||||
@ -157,17 +163,16 @@ class Order(_DECL_BASE):
|
||||
tzinfo=timezone.utc).timestamp() * 1000) if self.order_date else None,
|
||||
'order_filled_date': self.order_filled_date.strftime(DATETIME_PRINT_FORMAT)
|
||||
if self.order_filled_date else None,
|
||||
'order_filled_timestamp': int(self.order_filled_date.replace(
|
||||
tzinfo=timezone.utc).timestamp() * 1000) if self.order_filled_date else None,
|
||||
'order_type': self.order_type,
|
||||
'price': self.price,
|
||||
'ft_is_entry': self.ft_order_side == entry_side,
|
||||
'remaining': self.remaining,
|
||||
}
|
||||
})
|
||||
return resp
|
||||
|
||||
def close_bt_order(self, close_date: datetime, trade: 'LocalTrade'):
|
||||
self.order_filled_date = close_date
|
||||
self.filled = self.amount
|
||||
self.remaining = 0
|
||||
self.status = 'closed'
|
||||
self.ft_is_open = False
|
||||
if (self.ft_order_side == trade.entry_side
|
||||
@ -396,9 +401,9 @@ class LocalTrade():
|
||||
f'open_rate={self.open_rate:.8f}, open_since={open_since})'
|
||||
)
|
||||
|
||||
def to_json(self) -> Dict[str, Any]:
|
||||
filled_orders = self.select_filled_orders()
|
||||
orders = [order.to_json(self.entry_side) for order in filled_orders]
|
||||
def to_json(self, minified: bool = False) -> Dict[str, Any]:
|
||||
filled_orders = self.select_filled_or_open_orders()
|
||||
orders = [order.to_json(self.entry_side, minified) for order in filled_orders]
|
||||
|
||||
return {
|
||||
'trade_id': self.id,
|
||||
@ -871,14 +876,6 @@ class LocalTrade():
|
||||
return float(f"{profit_ratio:.8f}")
|
||||
|
||||
def recalc_trade_from_orders(self):
|
||||
# We need at least 2 entry orders for averaging amounts and rates.
|
||||
# TODO: this condition could probably be removed
|
||||
if len(self.select_filled_orders(self.entry_side)) < 2:
|
||||
self.stake_amount = self.amount * self.open_rate / self.leverage
|
||||
|
||||
# Just in case, still recalc open trade value
|
||||
self.recalc_open_trade_value()
|
||||
return
|
||||
|
||||
total_amount = 0.0
|
||||
total_stake = 0.0
|
||||
@ -948,6 +945,21 @@ class LocalTrade():
|
||||
and o.filled
|
||||
and o.status in NON_OPEN_EXCHANGE_STATES]
|
||||
|
||||
def select_filled_or_open_orders(self) -> List['Order']:
|
||||
"""
|
||||
Finds filled or open orders
|
||||
:param order_side: Side of the order (either 'buy', 'sell', or None)
|
||||
:return: array of Order objects
|
||||
"""
|
||||
return [o for o in self.orders if
|
||||
(
|
||||
o.ft_is_open is False
|
||||
and (o.filled or 0) > 0
|
||||
and o.status in NON_OPEN_EXCHANGE_STATES
|
||||
)
|
||||
or (o.ft_is_open is True and o.status is not None)
|
||||
]
|
||||
|
||||
@property
|
||||
def nr_of_successful_entries(self) -> int:
|
||||
"""
|
||||
|
@ -47,26 +47,7 @@ class StrategyResolver(IResolver):
|
||||
strategy: IStrategy = StrategyResolver._load_strategy(
|
||||
strategy_name, config=config,
|
||||
extra_dir=config.get('strategy_path'))
|
||||
|
||||
if strategy._ft_params_from_file:
|
||||
# Set parameters from Hyperopt results file
|
||||
params = strategy._ft_params_from_file
|
||||
strategy.minimal_roi = params.get('roi', getattr(strategy, 'minimal_roi', {}))
|
||||
|
||||
strategy.stoploss = params.get('stoploss', {}).get(
|
||||
'stoploss', getattr(strategy, 'stoploss', -0.1))
|
||||
trailing = params.get('trailing', {})
|
||||
strategy.trailing_stop = trailing.get(
|
||||
'trailing_stop', getattr(strategy, 'trailing_stop', False))
|
||||
strategy.trailing_stop_positive = trailing.get(
|
||||
'trailing_stop_positive', getattr(strategy, 'trailing_stop_positive', None))
|
||||
strategy.trailing_stop_positive_offset = trailing.get(
|
||||
'trailing_stop_positive_offset',
|
||||
getattr(strategy, 'trailing_stop_positive_offset', 0))
|
||||
strategy.trailing_only_offset_is_reached = trailing.get(
|
||||
'trailing_only_offset_is_reached',
|
||||
getattr(strategy, 'trailing_only_offset_is_reached', 0.0))
|
||||
|
||||
strategy.ft_load_params_from_file()
|
||||
# Set attributes
|
||||
# Check if we need to override configuration
|
||||
# (Attribute name, default, subkey)
|
||||
|
@ -120,6 +120,8 @@ class Stats(BaseModel):
|
||||
class DailyRecord(BaseModel):
|
||||
date: date
|
||||
abs_profit: float
|
||||
rel_profit: float
|
||||
starting_balance: float
|
||||
fiat_value: float
|
||||
trade_count: int
|
||||
|
||||
@ -166,7 +168,7 @@ class ShowConfig(BaseModel):
|
||||
trailing_stop_positive: Optional[float]
|
||||
trailing_stop_positive_offset: Optional[float]
|
||||
trailing_only_offset_is_reached: Optional[bool]
|
||||
unfilledtimeout: UnfilledTimeout
|
||||
unfilledtimeout: Optional[UnfilledTimeout] # Empty in webserver mode
|
||||
order_types: Optional[OrderTypes]
|
||||
use_custom_stoploss: Optional[bool]
|
||||
timeframe: Optional[str]
|
||||
|
@ -36,7 +36,8 @@ logger = logging.getLogger(__name__)
|
||||
# versions 2.xx -> futures/short branch
|
||||
# 2.14: Add entry/exit orders to trade response
|
||||
# 2.15: Add backtest history endpoints
|
||||
API_VERSION = 2.15
|
||||
# 2.16: Additional daily metrics
|
||||
API_VERSION = 2.16
|
||||
|
||||
# Public API, requires no auth.
|
||||
router_public = APIRouter()
|
||||
@ -86,7 +87,7 @@ def stats(rpc: RPC = Depends(get_rpc)):
|
||||
|
||||
@router.get('/daily', response_model=Daily, tags=['info'])
|
||||
def daily(timescale: int = 7, rpc: RPC = Depends(get_rpc), config=Depends(get_config)):
|
||||
return rpc._rpc_daily_profit(timescale, config['stake_currency'],
|
||||
return rpc._rpc_timeunit_profit(timescale, config['stake_currency'],
|
||||
config.get('fiat_display_currency', ''))
|
||||
|
||||
|
||||
|
59
freqtrade/rpc/discord.py
Normal file
59
freqtrade/rpc/discord.py
Normal file
@ -0,0 +1,59 @@
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
|
||||
from freqtrade.enums.rpcmessagetype import RPCMessageType
|
||||
from freqtrade.rpc import RPC
|
||||
from freqtrade.rpc.webhook import Webhook
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Discord(Webhook):
|
||||
def __init__(self, rpc: 'RPC', config: Dict[str, Any]):
|
||||
# super().__init__(rpc, config)
|
||||
self.rpc = rpc
|
||||
self.config = config
|
||||
self.strategy = config.get('strategy', '')
|
||||
self.timeframe = config.get('timeframe', '')
|
||||
|
||||
self._url = self.config['discord']['webhook_url']
|
||||
self._format = 'json'
|
||||
self._retries = 1
|
||||
self._retry_delay = 0.1
|
||||
|
||||
def cleanup(self) -> None:
|
||||
"""
|
||||
Cleanup pending module resources.
|
||||
This will do nothing for webhooks, they will simply not be called anymore
|
||||
"""
|
||||
pass
|
||||
|
||||
def send_msg(self, msg) -> None:
|
||||
logger.info(f"Sending discord message: {msg}")
|
||||
|
||||
if msg['type'].value in self.config['discord']:
|
||||
|
||||
msg['strategy'] = self.strategy
|
||||
msg['timeframe'] = self.timeframe
|
||||
fields = self.config['discord'].get(msg['type'].value)
|
||||
color = 0x0000FF
|
||||
if msg['type'] in (RPCMessageType.EXIT, RPCMessageType.EXIT_FILL):
|
||||
profit_ratio = msg.get('profit_ratio')
|
||||
color = (0x00FF00 if profit_ratio > 0 else 0xFF0000)
|
||||
|
||||
embeds = [{
|
||||
'title': f"Trade: {msg['pair']} {msg['type'].value}",
|
||||
'color': color,
|
||||
'fields': [],
|
||||
|
||||
}]
|
||||
for f in fields:
|
||||
for k, v in f.items():
|
||||
v = v.format(**msg)
|
||||
embeds[0]['fields'].append( # type: ignore
|
||||
{'name': k, 'value': v, 'inline': True})
|
||||
|
||||
# Send the message to discord channel
|
||||
payload = {'embeds': embeds}
|
||||
self._send_msg(payload)
|
@ -283,33 +283,57 @@ class RPC:
|
||||
columns.append('# Entries')
|
||||
return trades_list, columns, fiat_profit_sum
|
||||
|
||||
def _rpc_daily_profit(
|
||||
def _rpc_timeunit_profit(
|
||||
self, timescale: int,
|
||||
stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]:
|
||||
today = datetime.now(timezone.utc).date()
|
||||
profit_days: Dict[date, Dict] = {}
|
||||
stake_currency: str, fiat_display_currency: str,
|
||||
timeunit: str = 'days') -> Dict[str, Any]:
|
||||
"""
|
||||
:param timeunit: Valid entries are 'days', 'weeks', 'months'
|
||||
"""
|
||||
start_date = datetime.now(timezone.utc).date()
|
||||
if timeunit == 'weeks':
|
||||
# weekly
|
||||
start_date = start_date - timedelta(days=start_date.weekday()) # Monday
|
||||
if timeunit == 'months':
|
||||
start_date = start_date.replace(day=1)
|
||||
|
||||
def time_offset(step: int):
|
||||
if timeunit == 'months':
|
||||
return relativedelta(months=step)
|
||||
return timedelta(**{timeunit: step})
|
||||
|
||||
if not (isinstance(timescale, int) and timescale > 0):
|
||||
raise RPCException('timescale must be an integer greater than 0')
|
||||
|
||||
profit_units: Dict[date, Dict] = {}
|
||||
daily_stake = self._freqtrade.wallets.get_total_stake_amount()
|
||||
|
||||
for day in range(0, timescale):
|
||||
profitday = today - timedelta(days=day)
|
||||
trades = Trade.get_trades(trade_filter=[
|
||||
profitday = start_date - time_offset(day)
|
||||
# Only query for necessary columns for performance reasons.
|
||||
trades = Trade.query.session.query(Trade.close_profit_abs).filter(
|
||||
Trade.is_open.is_(False),
|
||||
Trade.close_date >= profitday,
|
||||
Trade.close_date < (profitday + timedelta(days=1))
|
||||
]).order_by(Trade.close_date).all()
|
||||
Trade.close_date < (profitday + time_offset(1))
|
||||
).order_by(Trade.close_date).all()
|
||||
|
||||
curdayprofit = sum(
|
||||
trade.close_profit_abs for trade in trades if trade.close_profit_abs is not None)
|
||||
profit_days[profitday] = {
|
||||
# Calculate this periods starting balance
|
||||
daily_stake = daily_stake - curdayprofit
|
||||
profit_units[profitday] = {
|
||||
'amount': curdayprofit,
|
||||
'trades': len(trades)
|
||||
'daily_stake': daily_stake,
|
||||
'rel_profit': round(curdayprofit / daily_stake, 8) if daily_stake > 0 else 0,
|
||||
'trades': len(trades),
|
||||
}
|
||||
|
||||
data = [
|
||||
{
|
||||
'date': key,
|
||||
'date': f"{key.year}-{key.month:02d}" if timeunit == 'months' else key,
|
||||
'abs_profit': value["amount"],
|
||||
'starting_balance': value["daily_stake"],
|
||||
'rel_profit': value["rel_profit"],
|
||||
'fiat_value': self._fiat_converter.convert_amount(
|
||||
value['amount'],
|
||||
stake_currency,
|
||||
@ -317,92 +341,7 @@ class RPC:
|
||||
) if self._fiat_converter else 0,
|
||||
'trade_count': value["trades"],
|
||||
}
|
||||
for key, value in profit_days.items()
|
||||
]
|
||||
return {
|
||||
'stake_currency': stake_currency,
|
||||
'fiat_display_currency': fiat_display_currency,
|
||||
'data': data
|
||||
}
|
||||
|
||||
def _rpc_weekly_profit(
|
||||
self, timescale: int,
|
||||
stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]:
|
||||
today = datetime.now(timezone.utc).date()
|
||||
first_iso_day_of_week = today - timedelta(days=today.weekday()) # Monday
|
||||
profit_weeks: Dict[date, Dict] = {}
|
||||
|
||||
if not (isinstance(timescale, int) and timescale > 0):
|
||||
raise RPCException('timescale must be an integer greater than 0')
|
||||
|
||||
for week in range(0, timescale):
|
||||
profitweek = first_iso_day_of_week - timedelta(weeks=week)
|
||||
trades = Trade.get_trades(trade_filter=[
|
||||
Trade.is_open.is_(False),
|
||||
Trade.close_date >= profitweek,
|
||||
Trade.close_date < (profitweek + timedelta(weeks=1))
|
||||
]).order_by(Trade.close_date).all()
|
||||
curweekprofit = sum(
|
||||
trade.close_profit_abs for trade in trades if trade.close_profit_abs is not None)
|
||||
profit_weeks[profitweek] = {
|
||||
'amount': curweekprofit,
|
||||
'trades': len(trades)
|
||||
}
|
||||
|
||||
data = [
|
||||
{
|
||||
'date': key,
|
||||
'abs_profit': value["amount"],
|
||||
'fiat_value': self._fiat_converter.convert_amount(
|
||||
value['amount'],
|
||||
stake_currency,
|
||||
fiat_display_currency
|
||||
) if self._fiat_converter else 0,
|
||||
'trade_count': value["trades"],
|
||||
}
|
||||
for key, value in profit_weeks.items()
|
||||
]
|
||||
return {
|
||||
'stake_currency': stake_currency,
|
||||
'fiat_display_currency': fiat_display_currency,
|
||||
'data': data
|
||||
}
|
||||
|
||||
def _rpc_monthly_profit(
|
||||
self, timescale: int,
|
||||
stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]:
|
||||
first_day_of_month = datetime.now(timezone.utc).date().replace(day=1)
|
||||
profit_months: Dict[date, Dict] = {}
|
||||
|
||||
if not (isinstance(timescale, int) and timescale > 0):
|
||||
raise RPCException('timescale must be an integer greater than 0')
|
||||
|
||||
for month in range(0, timescale):
|
||||
profitmonth = first_day_of_month - relativedelta(months=month)
|
||||
trades = Trade.get_trades(trade_filter=[
|
||||
Trade.is_open.is_(False),
|
||||
Trade.close_date >= profitmonth,
|
||||
Trade.close_date < (profitmonth + relativedelta(months=1))
|
||||
]).order_by(Trade.close_date).all()
|
||||
curmonthprofit = sum(
|
||||
trade.close_profit_abs for trade in trades if trade.close_profit_abs is not None)
|
||||
profit_months[profitmonth] = {
|
||||
'amount': curmonthprofit,
|
||||
'trades': len(trades)
|
||||
}
|
||||
|
||||
data = [
|
||||
{
|
||||
'date': f"{key.year}-{key.month:02d}",
|
||||
'abs_profit': value["amount"],
|
||||
'fiat_value': self._fiat_converter.convert_amount(
|
||||
value['amount'],
|
||||
stake_currency,
|
||||
fiat_display_currency
|
||||
) if self._fiat_converter else 0,
|
||||
'trade_count': value["trades"],
|
||||
}
|
||||
for key, value in profit_months.items()
|
||||
for key, value in profit_units.items()
|
||||
]
|
||||
return {
|
||||
'stake_currency': stake_currency,
|
||||
|
@ -27,6 +27,12 @@ class RPCManager:
|
||||
from freqtrade.rpc.telegram import Telegram
|
||||
self.registered_modules.append(Telegram(self._rpc, config))
|
||||
|
||||
# Enable discord
|
||||
if config.get('discord', {}).get('enabled', False):
|
||||
logger.info('Enabling rpc.discord ...')
|
||||
from freqtrade.rpc.discord import Discord
|
||||
self.registered_modules.append(Discord(self._rpc, config))
|
||||
|
||||
# Enable Webhook
|
||||
if config.get('webhook', {}).get('enabled', False):
|
||||
logger.info('Enabling rpc.webhook ...')
|
||||
|
@ -6,6 +6,7 @@ This module manage Telegram communication
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from datetime import date, datetime, timedelta
|
||||
from functools import partial
|
||||
from html import escape
|
||||
@ -37,6 +38,15 @@ logger.debug('Included module rpc.telegram ...')
|
||||
MAX_TELEGRAM_MESSAGE_LENGTH = 4096
|
||||
|
||||
|
||||
@dataclass
|
||||
class TimeunitMappings:
|
||||
header: str
|
||||
message: str
|
||||
message2: str
|
||||
callback: str
|
||||
default: int
|
||||
|
||||
|
||||
def authorized_only(command_handler: Callable[..., None]) -> Callable[..., Any]:
|
||||
"""
|
||||
Decorator to check if the message comes from the correct chat_id
|
||||
@ -427,7 +437,10 @@ class Telegram(RPCHandler):
|
||||
first_avg = filled_orders[0]["safe_price"]
|
||||
|
||||
for x, order in enumerate(filled_orders):
|
||||
if order['is_open'] is True:
|
||||
continue
|
||||
wording = 'Entry' if order['ft_is_entry'] else 'Exit'
|
||||
|
||||
cur_entry_datetime = arrow.get(order["order_filled_date"])
|
||||
cur_entry_amount = order["filled"] or order["amount"]
|
||||
cur_entry_average = order["safe_price"]
|
||||
@ -605,6 +618,60 @@ class Telegram(RPCHandler):
|
||||
except RPCException as e:
|
||||
self._send_msg(str(e))
|
||||
|
||||
@authorized_only
|
||||
def _timeunit_stats(self, update: Update, context: CallbackContext, unit: str) -> None:
|
||||
"""
|
||||
Handler for /daily <n>
|
||||
Returns a daily profit (in BTC) over the last n days.
|
||||
:param bot: telegram bot
|
||||
:param update: message update
|
||||
:return: None
|
||||
"""
|
||||
|
||||
vals = {
|
||||
'days': TimeunitMappings('Day', 'Daily', 'days', 'update_daily', 7),
|
||||
'weeks': TimeunitMappings('Monday', 'Weekly', 'weeks (starting from Monday)',
|
||||
'update_weekly', 8),
|
||||
'months': TimeunitMappings('Month', 'Monthly', 'months', 'update_monthly', 6),
|
||||
}
|
||||
val = vals[unit]
|
||||
|
||||
stake_cur = self._config['stake_currency']
|
||||
fiat_disp_cur = self._config.get('fiat_display_currency', '')
|
||||
try:
|
||||
timescale = int(context.args[0]) if context.args else val.default
|
||||
except (TypeError, ValueError, IndexError):
|
||||
timescale = val.default
|
||||
try:
|
||||
stats = self._rpc._rpc_timeunit_profit(
|
||||
timescale,
|
||||
stake_cur,
|
||||
fiat_disp_cur,
|
||||
unit
|
||||
)
|
||||
stats_tab = tabulate(
|
||||
[[f"{period['date']} ({period['trade_count']})",
|
||||
f"{round_coin_value(period['abs_profit'], stats['stake_currency'])}",
|
||||
f"{period['fiat_value']:.2f} {stats['fiat_display_currency']}",
|
||||
f"{period['rel_profit']:.2%}",
|
||||
] for period in stats['data']],
|
||||
headers=[
|
||||
f"{val.header} (count)",
|
||||
f'{stake_cur}',
|
||||
f'{fiat_disp_cur}',
|
||||
'Profit %',
|
||||
'Trades',
|
||||
],
|
||||
tablefmt='simple')
|
||||
message = (
|
||||
f'<b>{val.message} Profit over the last {timescale} {val.message2}</b>:\n'
|
||||
f'<pre>{stats_tab}</pre>'
|
||||
)
|
||||
self._send_msg(message, parse_mode=ParseMode.HTML, reload_able=True,
|
||||
callback_path=val.callback, query=update.callback_query)
|
||||
except RPCException as e:
|
||||
self._send_msg(str(e))
|
||||
|
||||
@authorized_only
|
||||
def _daily(self, update: Update, context: CallbackContext) -> None:
|
||||
"""
|
||||
@ -614,35 +681,7 @@ class Telegram(RPCHandler):
|
||||
:param update: message update
|
||||
:return: None
|
||||
"""
|
||||
stake_cur = self._config['stake_currency']
|
||||
fiat_disp_cur = self._config.get('fiat_display_currency', '')
|
||||
try:
|
||||
timescale = int(context.args[0]) if context.args else 7
|
||||
except (TypeError, ValueError, IndexError):
|
||||
timescale = 7
|
||||
try:
|
||||
stats = self._rpc._rpc_daily_profit(
|
||||
timescale,
|
||||
stake_cur,
|
||||
fiat_disp_cur
|
||||
)
|
||||
stats_tab = tabulate(
|
||||
[[day['date'],
|
||||
f"{round_coin_value(day['abs_profit'], stats['stake_currency'])}",
|
||||
f"{day['fiat_value']:.3f} {stats['fiat_display_currency']}",
|
||||
f"{day['trade_count']} trades"] for day in stats['data']],
|
||||
headers=[
|
||||
'Day',
|
||||
f'Profit {stake_cur}',
|
||||
f'Profit {fiat_disp_cur}',
|
||||
'Trades',
|
||||
],
|
||||
tablefmt='simple')
|
||||
message = f'<b>Daily Profit over the last {timescale} days</b>:\n<pre>{stats_tab}</pre>'
|
||||
self._send_msg(message, parse_mode=ParseMode.HTML, reload_able=True,
|
||||
callback_path="update_daily", query=update.callback_query)
|
||||
except RPCException as e:
|
||||
self._send_msg(str(e))
|
||||
self._timeunit_stats(update, context, 'days')
|
||||
|
||||
@authorized_only
|
||||
def _weekly(self, update: Update, context: CallbackContext) -> None:
|
||||
@ -653,36 +692,7 @@ class Telegram(RPCHandler):
|
||||
:param update: message update
|
||||
:return: None
|
||||
"""
|
||||
stake_cur = self._config['stake_currency']
|
||||
fiat_disp_cur = self._config.get('fiat_display_currency', '')
|
||||
try:
|
||||
timescale = int(context.args[0]) if context.args else 8
|
||||
except (TypeError, ValueError, IndexError):
|
||||
timescale = 8
|
||||
try:
|
||||
stats = self._rpc._rpc_weekly_profit(
|
||||
timescale,
|
||||
stake_cur,
|
||||
fiat_disp_cur
|
||||
)
|
||||
stats_tab = tabulate(
|
||||
[[week['date'],
|
||||
f"{round_coin_value(week['abs_profit'], stats['stake_currency'])}",
|
||||
f"{week['fiat_value']:.3f} {stats['fiat_display_currency']}",
|
||||
f"{week['trade_count']} trades"] for week in stats['data']],
|
||||
headers=[
|
||||
'Monday',
|
||||
f'Profit {stake_cur}',
|
||||
f'Profit {fiat_disp_cur}',
|
||||
'Trades',
|
||||
],
|
||||
tablefmt='simple')
|
||||
message = f'<b>Weekly Profit over the last {timescale} weeks ' \
|
||||
f'(starting from Monday)</b>:\n<pre>{stats_tab}</pre> '
|
||||
self._send_msg(message, parse_mode=ParseMode.HTML, reload_able=True,
|
||||
callback_path="update_weekly", query=update.callback_query)
|
||||
except RPCException as e:
|
||||
self._send_msg(str(e))
|
||||
self._timeunit_stats(update, context, 'weeks')
|
||||
|
||||
@authorized_only
|
||||
def _monthly(self, update: Update, context: CallbackContext) -> None:
|
||||
@ -693,36 +703,7 @@ class Telegram(RPCHandler):
|
||||
:param update: message update
|
||||
:return: None
|
||||
"""
|
||||
stake_cur = self._config['stake_currency']
|
||||
fiat_disp_cur = self._config.get('fiat_display_currency', '')
|
||||
try:
|
||||
timescale = int(context.args[0]) if context.args else 6
|
||||
except (TypeError, ValueError, IndexError):
|
||||
timescale = 6
|
||||
try:
|
||||
stats = self._rpc._rpc_monthly_profit(
|
||||
timescale,
|
||||
stake_cur,
|
||||
fiat_disp_cur
|
||||
)
|
||||
stats_tab = tabulate(
|
||||
[[month['date'],
|
||||
f"{round_coin_value(month['abs_profit'], stats['stake_currency'])}",
|
||||
f"{month['fiat_value']:.3f} {stats['fiat_display_currency']}",
|
||||
f"{month['trade_count']} trades"] for month in stats['data']],
|
||||
headers=[
|
||||
'Month',
|
||||
f'Profit {stake_cur}',
|
||||
f'Profit {fiat_disp_cur}',
|
||||
'Trades',
|
||||
],
|
||||
tablefmt='simple')
|
||||
message = f'<b>Monthly Profit over the last {timescale} months' \
|
||||
f'</b>:\n<pre>{stats_tab}</pre> '
|
||||
self._send_msg(message, parse_mode=ParseMode.HTML, reload_able=True,
|
||||
callback_path="update_monthly", query=update.callback_query)
|
||||
except RPCException as e:
|
||||
self._send_msg(str(e))
|
||||
self._timeunit_stats(update, context, 'months')
|
||||
|
||||
@authorized_only
|
||||
def _profit(self, update: Update, context: CallbackContext) -> None:
|
||||
@ -827,7 +808,7 @@ class Telegram(RPCHandler):
|
||||
headers=['Exit Reason', 'Exits', 'Wins', 'Losses']
|
||||
)
|
||||
if len(exit_reasons_tabulate) > 25:
|
||||
self._send_msg(exit_reasons_msg, ParseMode.MARKDOWN)
|
||||
self._send_msg(f"```\n{exit_reasons_msg}```", ParseMode.MARKDOWN)
|
||||
exit_reasons_msg = ''
|
||||
|
||||
durations = stats['durations']
|
||||
|
@ -4,9 +4,8 @@ This module defines a base class for auto-hyperoptable strategies.
|
||||
"""
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Iterator, List, Tuple
|
||||
from typing import Any, Dict, Iterator, List, Tuple, Type, Union
|
||||
|
||||
from freqtrade.enums import RunMode
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.misc import deep_merge_dicts, json_load
|
||||
from freqtrade.optimize.hyperopt_tools import HyperoptTools
|
||||
@ -31,7 +30,10 @@ class HyperStrategyMixin:
|
||||
self.ft_sell_params: List[BaseParameter] = []
|
||||
self.ft_protection_params: List[BaseParameter] = []
|
||||
|
||||
self._load_hyper_params(config.get('runmode') == RunMode.HYPEROPT)
|
||||
params = self.load_params_from_file()
|
||||
params = params.get('params', {})
|
||||
self._ft_params_from_file = params
|
||||
# Init/loading of parameters is done as part of ft_bot_start().
|
||||
|
||||
def enumerate_parameters(self, category: str = None) -> Iterator[Tuple[str, BaseParameter]]:
|
||||
"""
|
||||
@ -51,28 +53,13 @@ class HyperStrategyMixin:
|
||||
for par in params:
|
||||
yield par.name, par
|
||||
|
||||
@classmethod
|
||||
def detect_parameters(cls, category: str) -> Iterator[Tuple[str, BaseParameter]]:
|
||||
""" Detect all parameters for 'category' """
|
||||
for attr_name in dir(cls):
|
||||
if not attr_name.startswith('__'): # Ignore internals, not strictly necessary.
|
||||
attr = getattr(cls, attr_name)
|
||||
if issubclass(attr.__class__, BaseParameter):
|
||||
if (attr_name.startswith(category + '_')
|
||||
and attr.category is not None and attr.category != category):
|
||||
raise OperationalException(
|
||||
f'Inconclusive parameter name {attr_name}, category: {attr.category}.')
|
||||
if (category == attr.category or
|
||||
(attr_name.startswith(category + '_') and attr.category is None)):
|
||||
yield attr_name, attr
|
||||
|
||||
@classmethod
|
||||
def detect_all_parameters(cls) -> Dict:
|
||||
""" Detect all parameters and return them as a list"""
|
||||
params: Dict[str, Any] = {
|
||||
'buy': list(cls.detect_parameters('buy')),
|
||||
'sell': list(cls.detect_parameters('sell')),
|
||||
'protection': list(cls.detect_parameters('protection')),
|
||||
'buy': list(detect_parameters(cls, 'buy')),
|
||||
'sell': list(detect_parameters(cls, 'sell')),
|
||||
'protection': list(detect_parameters(cls, 'protection')),
|
||||
}
|
||||
params.update({
|
||||
'count': len(params['buy'] + params['sell'] + params['protection'])
|
||||
@ -80,21 +67,49 @@ class HyperStrategyMixin:
|
||||
|
||||
return params
|
||||
|
||||
def _load_hyper_params(self, hyperopt: bool = False) -> None:
|
||||
def ft_load_params_from_file(self) -> None:
|
||||
"""
|
||||
Load Parameters from parameter file
|
||||
Should/must run before config values are loaded in strategy_resolver.
|
||||
"""
|
||||
if self._ft_params_from_file:
|
||||
# Set parameters from Hyperopt results file
|
||||
params = self._ft_params_from_file
|
||||
self.minimal_roi = params.get('roi', getattr(self, 'minimal_roi', {}))
|
||||
|
||||
self.stoploss = params.get('stoploss', {}).get(
|
||||
'stoploss', getattr(self, 'stoploss', -0.1))
|
||||
trailing = params.get('trailing', {})
|
||||
self.trailing_stop = trailing.get(
|
||||
'trailing_stop', getattr(self, 'trailing_stop', False))
|
||||
self.trailing_stop_positive = trailing.get(
|
||||
'trailing_stop_positive', getattr(self, 'trailing_stop_positive', None))
|
||||
self.trailing_stop_positive_offset = trailing.get(
|
||||
'trailing_stop_positive_offset',
|
||||
getattr(self, 'trailing_stop_positive_offset', 0))
|
||||
self.trailing_only_offset_is_reached = trailing.get(
|
||||
'trailing_only_offset_is_reached',
|
||||
getattr(self, 'trailing_only_offset_is_reached', 0.0))
|
||||
|
||||
def ft_load_hyper_params(self, hyperopt: bool = False) -> None:
|
||||
"""
|
||||
Load Hyperoptable parameters
|
||||
Prevalence:
|
||||
* Parameters from parameter file
|
||||
* Parameters defined in parameters objects (buy_params, sell_params, ...)
|
||||
* Parameter defaults
|
||||
"""
|
||||
params = self.load_params_from_file()
|
||||
params = params.get('params', {})
|
||||
self._ft_params_from_file = params
|
||||
buy_params = deep_merge_dicts(params.get('buy', {}), getattr(self, 'buy_params', {}))
|
||||
sell_params = deep_merge_dicts(params.get('sell', {}), getattr(self, 'sell_params', {}))
|
||||
protection_params = deep_merge_dicts(params.get('protection', {}),
|
||||
|
||||
buy_params = deep_merge_dicts(self._ft_params_from_file.get('buy', {}),
|
||||
getattr(self, 'buy_params', {}))
|
||||
sell_params = deep_merge_dicts(self._ft_params_from_file.get('sell', {}),
|
||||
getattr(self, 'sell_params', {}))
|
||||
protection_params = deep_merge_dicts(self._ft_params_from_file.get('protection', {}),
|
||||
getattr(self, 'protection_params', {}))
|
||||
|
||||
self._load_params(buy_params, 'buy', hyperopt)
|
||||
self._load_params(sell_params, 'sell', hyperopt)
|
||||
self._load_params(protection_params, 'protection', hyperopt)
|
||||
self._ft_load_params(buy_params, 'buy', hyperopt)
|
||||
self._ft_load_params(sell_params, 'sell', hyperopt)
|
||||
self._ft_load_params(protection_params, 'protection', hyperopt)
|
||||
|
||||
def load_params_from_file(self) -> Dict:
|
||||
filename_str = getattr(self, '__file__', '')
|
||||
@ -117,7 +132,7 @@ class HyperStrategyMixin:
|
||||
|
||||
return {}
|
||||
|
||||
def _load_params(self, params: Dict, space: str, hyperopt: bool = False) -> None:
|
||||
def _ft_load_params(self, params: Dict, space: str, hyperopt: bool = False) -> None:
|
||||
"""
|
||||
Set optimizable parameter values.
|
||||
:param params: Dictionary with new parameter values.
|
||||
@ -126,7 +141,7 @@ class HyperStrategyMixin:
|
||||
logger.info(f"No params for {space} found, using default values.")
|
||||
param_container: List[BaseParameter] = getattr(self, f"ft_{space}_params")
|
||||
|
||||
for attr_name, attr in self.detect_parameters(space):
|
||||
for attr_name, attr in detect_parameters(self, space):
|
||||
attr.name = attr_name
|
||||
attr.in_space = hyperopt and HyperoptTools.has_space(self.config, space)
|
||||
if not attr.category:
|
||||
@ -157,3 +172,25 @@ class HyperStrategyMixin:
|
||||
if not p.optimize or not p.in_space:
|
||||
params[p.category][name] = p.value
|
||||
return params
|
||||
|
||||
|
||||
def detect_parameters(
|
||||
obj: Union[HyperStrategyMixin, Type[HyperStrategyMixin]],
|
||||
category: str
|
||||
) -> Iterator[Tuple[str, BaseParameter]]:
|
||||
"""
|
||||
Detect all parameters for 'category' for "obj"
|
||||
:param obj: Strategy object or class
|
||||
:param category: category - usually `'buy', 'sell', 'protection',...
|
||||
"""
|
||||
for attr_name in dir(obj):
|
||||
if not attr_name.startswith('__'): # Ignore internals, not strictly necessary.
|
||||
attr = getattr(obj, attr_name)
|
||||
if issubclass(attr.__class__, BaseParameter):
|
||||
if (attr_name.startswith(category + '_')
|
||||
and attr.category is not None and attr.category != category):
|
||||
raise OperationalException(
|
||||
f'Inconclusive parameter name {attr_name}, category: {attr.category}.')
|
||||
if (category == attr.category or
|
||||
(attr_name.startswith(category + '_') and attr.category is None)):
|
||||
yield attr_name, attr
|
||||
|
@ -14,6 +14,7 @@ from freqtrade.constants import ListPairsWithTimeframes
|
||||
from freqtrade.data.dataprovider import DataProvider
|
||||
from freqtrade.enums import (CandleType, ExitCheckTuple, ExitType, SignalDirection, SignalTagType,
|
||||
SignalType, TradingMode)
|
||||
from freqtrade.enums.runmode import RunMode
|
||||
from freqtrade.exceptions import OperationalException, StrategyError
|
||||
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date, timeframe_to_seconds
|
||||
from freqtrade.persistence import Order, PairLocks, Trade
|
||||
@ -151,6 +152,8 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
"""
|
||||
strategy_safe_wrapper(self.bot_start)()
|
||||
|
||||
self.ft_load_hyper_params(self.config.get('runmode') == RunMode.HYPEROPT)
|
||||
|
||||
@abstractmethod
|
||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""
|
||||
@ -284,8 +287,9 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
|
||||
:param pair: Pair that's about to be bought/shorted.
|
||||
:param order_type: Order type (as configured in order_types). usually limit or market.
|
||||
:param amount: Amount in target (quote) currency that's going to be traded.
|
||||
:param amount: Amount in target (base) currency that's going to be traded.
|
||||
:param rate: Rate that's going to be used when using limit orders
|
||||
or current rate for market orders.
|
||||
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
|
||||
:param current_time: datetime object, containing the current datetime
|
||||
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
|
||||
@ -311,8 +315,9 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
:param pair: Pair for trade that's about to be exited.
|
||||
:param trade: trade object.
|
||||
:param order_type: Order type (as configured in order_types). usually limit or market.
|
||||
:param amount: Amount in quote currency.
|
||||
:param amount: Amount in base currency.
|
||||
:param rate: Rate that's going to be used when using limit orders
|
||||
or current rate for market orders.
|
||||
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
|
||||
:param exit_reason: Exit reason.
|
||||
Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss',
|
||||
@ -515,8 +520,8 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
return current_order_rate
|
||||
|
||||
def leverage(self, pair: str, current_time: datetime, current_rate: float,
|
||||
proposed_leverage: float, max_leverage: float, side: str,
|
||||
**kwargs) -> float:
|
||||
proposed_leverage: float, max_leverage: float, entry_tag: Optional[str],
|
||||
side: str, **kwargs) -> float:
|
||||
"""
|
||||
Customize leverage for each new trade. This method is only called in futures mode.
|
||||
|
||||
@ -525,6 +530,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
:param current_rate: Rate, calculated based on pricing settings in exit_pricing.
|
||||
:param proposed_leverage: A leverage proposed by the bot.
|
||||
:param max_leverage: Max leverage allowed on this pair
|
||||
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
|
||||
:param side: 'long' or 'short' - indicating the direction of the proposed trade
|
||||
:return: A leverage amount, which is between 1.0 and max_leverage.
|
||||
"""
|
||||
|
@ -159,8 +159,9 @@ def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: f
|
||||
|
||||
:param pair: Pair that's about to be bought/shorted.
|
||||
:param order_type: Order type (as configured in order_types). usually limit or market.
|
||||
:param amount: Amount in target (quote) currency that's going to be traded.
|
||||
:param amount: Amount in target (base) currency that's going to be traded.
|
||||
:param rate: Rate that's going to be used when using limit orders
|
||||
or current rate for market orders.
|
||||
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
|
||||
:param current_time: datetime object, containing the current datetime
|
||||
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
|
||||
@ -175,7 +176,7 @@ def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount:
|
||||
rate: float, time_in_force: str, exit_reason: str,
|
||||
current_time: 'datetime', **kwargs) -> bool:
|
||||
"""
|
||||
Called right before placing a regular sell order.
|
||||
Called right before placing a regular exit order.
|
||||
Timing for this function is critical, so avoid doing heavy computations or
|
||||
network requests in this method.
|
||||
|
||||
@ -183,18 +184,19 @@ def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount:
|
||||
|
||||
When not implemented by a strategy, returns True (always confirming).
|
||||
|
||||
:param pair: Pair that's currently analyzed
|
||||
:param pair: Pair for trade that's about to be exited.
|
||||
:param trade: trade object.
|
||||
:param order_type: Order type (as configured in order_types). usually limit or market.
|
||||
:param amount: Amount in quote currency.
|
||||
:param amount: Amount in base currency.
|
||||
:param rate: Rate that's going to be used when using limit orders
|
||||
or current rate for market orders.
|
||||
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
|
||||
:param exit_reason: Exit reason.
|
||||
Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss',
|
||||
'exit_signal', 'force_exit', 'emergency_exit']
|
||||
:param current_time: datetime object, containing the current datetime
|
||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||
:return bool: When True is returned, then the exit-order is placed on the exchange.
|
||||
:return bool: When True, then the exit-order is placed on the exchange.
|
||||
False aborts the process
|
||||
"""
|
||||
return True
|
||||
@ -277,8 +279,8 @@ def adjust_trade_position(self, trade: 'Trade', current_time: datetime,
|
||||
return None
|
||||
|
||||
def leverage(self, pair: str, current_time: datetime, current_rate: float,
|
||||
proposed_leverage: float, max_leverage: float, side: str,
|
||||
**kwargs) -> float:
|
||||
proposed_leverage: float, max_leverage: float, entry_tag: Optional[str],
|
||||
side: str, **kwargs) -> float:
|
||||
"""
|
||||
Customize leverage for each new trade. This method is only called in futures mode.
|
||||
|
||||
@ -287,6 +289,7 @@ def leverage(self, pair: str, current_time: datetime, current_rate: float,
|
||||
:param current_rate: Rate, calculated based on pricing settings in exit_pricing.
|
||||
:param proposed_leverage: A leverage proposed by the bot.
|
||||
:param max_leverage: Max leverage allowed on this pair
|
||||
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
|
||||
:param side: 'long' or 'short' - indicating the direction of the proposed trade
|
||||
:return: A leverage amount, which is between 1.0 and max_leverage.
|
||||
"""
|
||||
|
@ -7,7 +7,7 @@
|
||||
coveralls==3.3.1
|
||||
flake8==4.0.1
|
||||
flake8-tidy-imports==4.8.0
|
||||
mypy==0.950
|
||||
mypy==0.961
|
||||
pre-commit==2.19.0
|
||||
pytest==7.1.2
|
||||
pytest-asyncio==0.18.3
|
||||
@ -23,7 +23,7 @@ nbconvert==6.5.0
|
||||
|
||||
# mypy types
|
||||
types-cachetools==5.0.1
|
||||
types-filelock==3.2.6
|
||||
types-requests==2.27.27
|
||||
types-filelock==3.2.7
|
||||
types-requests==2.27.30
|
||||
types-tabulate==0.8.9
|
||||
types-python-dateutil==2.8.16
|
||||
types-python-dateutil==2.8.17
|
||||
|
@ -5,5 +5,5 @@
|
||||
scipy==1.8.1
|
||||
scikit-learn==1.1.1
|
||||
scikit-optimize==0.9.0
|
||||
filelock==3.7.0
|
||||
filelock==3.7.1
|
||||
progressbar2==4.0.0
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Include all requirements to run the bot.
|
||||
-r requirements.txt
|
||||
|
||||
plotly==5.8.0
|
||||
plotly==5.8.2
|
||||
|
@ -2,17 +2,17 @@ numpy==1.22.4
|
||||
pandas==1.4.2
|
||||
pandas-ta==0.3.14b
|
||||
|
||||
ccxt==1.83.62
|
||||
ccxt==1.87.12
|
||||
# Pin cryptography for now due to rust build errors with piwheels
|
||||
cryptography==37.0.2
|
||||
aiohttp==3.8.1
|
||||
SQLAlchemy==1.4.36
|
||||
python-telegram-bot==13.11
|
||||
SQLAlchemy==1.4.37
|
||||
python-telegram-bot==13.12
|
||||
arrow==1.2.2
|
||||
cachetools==4.2.2
|
||||
requests==2.27.1
|
||||
requests==2.28.0
|
||||
urllib3==1.26.9
|
||||
jsonschema==4.5.1
|
||||
jsonschema==4.6.0
|
||||
TA-Lib==0.4.24
|
||||
technical==1.3.0
|
||||
tabulate==0.8.9
|
||||
@ -28,7 +28,7 @@ py_find_1st==1.1.5
|
||||
# Load ticker files 30% faster
|
||||
python-rapidjson==1.6
|
||||
# Properly format api responses
|
||||
orjson==3.6.8
|
||||
orjson==3.7.2
|
||||
|
||||
# Notify systemd
|
||||
sdnotify==0.3.2
|
||||
|
@ -261,7 +261,7 @@ class FtRestClient():
|
||||
}
|
||||
return self._post("forcebuy", data=data)
|
||||
|
||||
def force_enter(self, pair, side, price=None):
|
||||
def forceenter(self, pair, side, price=None):
|
||||
"""Force entering a trade
|
||||
|
||||
:param pair: Pair to buy (ETH/BTC)
|
||||
@ -273,7 +273,7 @@ class FtRestClient():
|
||||
"side": side,
|
||||
"price": price,
|
||||
}
|
||||
return self._post("force_enter", data=data)
|
||||
return self._post("forceenter", data=data)
|
||||
|
||||
def forceexit(self, tradeid):
|
||||
"""Force-exit a trade.
|
||||
|
4
setup.sh
4
setup.sh
@ -87,6 +87,10 @@ function updateenv() {
|
||||
echo "Failed installing Freqtrade"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Installing freqUI"
|
||||
freqtrade install-ui
|
||||
|
||||
echo "pip install completed"
|
||||
echo
|
||||
if [[ $dev =~ ^[Yy]$ ]]; then
|
||||
|
@ -325,7 +325,7 @@ def create_mock_trades_with_leverage(fee, use_db: bool = True):
|
||||
Trade.query.session.flush()
|
||||
|
||||
|
||||
def create_mock_trades_usdt(fee, use_db: bool = True):
|
||||
def create_mock_trades_usdt(fee, is_short: Optional[bool] = False, use_db: bool = True):
|
||||
"""
|
||||
Create some fake trades ...
|
||||
"""
|
||||
@ -335,26 +335,29 @@ def create_mock_trades_usdt(fee, use_db: bool = True):
|
||||
else:
|
||||
LocalTrade.add_bt_trade(trade)
|
||||
|
||||
is_short1 = is_short if is_short is not None else True
|
||||
is_short2 = is_short if is_short is not None else False
|
||||
|
||||
# Simulate dry_run entries
|
||||
trade = mock_trade_usdt_1(fee)
|
||||
trade = mock_trade_usdt_1(fee, is_short1)
|
||||
add_trade(trade)
|
||||
|
||||
trade = mock_trade_usdt_2(fee)
|
||||
trade = mock_trade_usdt_2(fee, is_short1)
|
||||
add_trade(trade)
|
||||
|
||||
trade = mock_trade_usdt_3(fee)
|
||||
trade = mock_trade_usdt_3(fee, is_short1)
|
||||
add_trade(trade)
|
||||
|
||||
trade = mock_trade_usdt_4(fee)
|
||||
trade = mock_trade_usdt_4(fee, is_short2)
|
||||
add_trade(trade)
|
||||
|
||||
trade = mock_trade_usdt_5(fee)
|
||||
trade = mock_trade_usdt_5(fee, is_short2)
|
||||
add_trade(trade)
|
||||
|
||||
trade = mock_trade_usdt_6(fee)
|
||||
trade = mock_trade_usdt_6(fee, is_short1)
|
||||
add_trade(trade)
|
||||
|
||||
trade = mock_trade_usdt_7(fee)
|
||||
trade = mock_trade_usdt_7(fee, is_short1)
|
||||
add_trade(trade)
|
||||
if use_db:
|
||||
Trade.commit()
|
||||
|
@ -6,47 +6,84 @@ from freqtrade.persistence.models import Order, Trade
|
||||
MOCK_TRADE_COUNT = 6
|
||||
|
||||
|
||||
def mock_order_usdt_1():
|
||||
def entry_side(is_short: bool):
|
||||
return "sell" if is_short else "buy"
|
||||
|
||||
|
||||
def exit_side(is_short: bool):
|
||||
return "buy" if is_short else "sell"
|
||||
|
||||
|
||||
def direc(is_short: bool):
|
||||
return "short" if is_short else "long"
|
||||
|
||||
|
||||
def mock_order_usdt_1(is_short: bool):
|
||||
return {
|
||||
'id': '1234',
|
||||
'symbol': 'ADA/USDT',
|
||||
'id': f'prod_entry_1_{direc(is_short)}',
|
||||
'symbol': 'LTC/USDT',
|
||||
'status': 'closed',
|
||||
'side': 'buy',
|
||||
'side': entry_side(is_short),
|
||||
'type': 'limit',
|
||||
'price': 2.0,
|
||||
'amount': 10.0,
|
||||
'filled': 10.0,
|
||||
'price': 10.0,
|
||||
'amount': 2.0,
|
||||
'filled': 2.0,
|
||||
'remaining': 0.0,
|
||||
}
|
||||
|
||||
|
||||
def mock_trade_usdt_1(fee):
|
||||
def mock_order_usdt_1_exit(is_short: bool):
|
||||
return {
|
||||
'id': f'prod_exit_1_{direc(is_short)}',
|
||||
'symbol': 'LTC/USDT',
|
||||
'status': 'closed',
|
||||
'side': exit_side(is_short),
|
||||
'type': 'limit',
|
||||
'price': 8.0,
|
||||
'amount': 2.0,
|
||||
'filled': 2.0,
|
||||
'remaining': 0.0,
|
||||
}
|
||||
|
||||
|
||||
def mock_trade_usdt_1(fee, is_short: bool):
|
||||
"""
|
||||
Simulate prod entry with open sell order
|
||||
"""
|
||||
trade = Trade(
|
||||
pair='ADA/USDT',
|
||||
pair='LTC/USDT',
|
||||
stake_amount=20.0,
|
||||
amount=10.0,
|
||||
amount_requested=10.0,
|
||||
amount=2.0,
|
||||
amount_requested=2.0,
|
||||
open_date=datetime.now(tz=timezone.utc) - timedelta(days=2, minutes=20),
|
||||
close_date=datetime.now(tz=timezone.utc) - timedelta(days=2, minutes=5),
|
||||
fee_open=fee.return_value,
|
||||
fee_close=fee.return_value,
|
||||
is_open=True,
|
||||
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=17),
|
||||
open_rate=2.0,
|
||||
is_open=False,
|
||||
open_rate=10.0,
|
||||
close_rate=8.0,
|
||||
close_profit=-0.2,
|
||||
close_profit_abs=-4.0,
|
||||
exchange='binance',
|
||||
open_order_id='dry_run_buy_12345',
|
||||
strategy='StrategyTestV2',
|
||||
strategy='SampleStrategy',
|
||||
open_order_id=f'prod_exit_1_{direc(is_short)}',
|
||||
timeframe=5,
|
||||
is_short=is_short,
|
||||
)
|
||||
o = Order.parse_from_ccxt_object(mock_order_usdt_1(), 'ADA/USDT', 'buy')
|
||||
o = Order.parse_from_ccxt_object(mock_order_usdt_1(is_short), 'LTC/USDT', entry_side(is_short))
|
||||
trade.orders.append(o)
|
||||
o = Order.parse_from_ccxt_object(mock_order_usdt_1_exit(is_short),
|
||||
'LTC/USDT', exit_side(is_short))
|
||||
trade.orders.append(o)
|
||||
return trade
|
||||
|
||||
|
||||
def mock_order_usdt_2():
|
||||
def mock_order_usdt_2(is_short: bool):
|
||||
return {
|
||||
'id': '1235',
|
||||
'id': f'1235_{direc(is_short)}',
|
||||
'symbol': 'ETC/USDT',
|
||||
'status': 'closed',
|
||||
'side': 'buy',
|
||||
'side': entry_side(is_short),
|
||||
'type': 'limit',
|
||||
'price': 2.0,
|
||||
'amount': 100.0,
|
||||
@ -55,12 +92,12 @@ def mock_order_usdt_2():
|
||||
}
|
||||
|
||||
|
||||
def mock_order_usdt_2_sell():
|
||||
def mock_order_usdt_2_exit(is_short: bool):
|
||||
return {
|
||||
'id': '12366',
|
||||
'id': f'12366_{direc(is_short)}',
|
||||
'symbol': 'ETC/USDT',
|
||||
'status': 'closed',
|
||||
'side': 'sell',
|
||||
'side': exit_side(is_short),
|
||||
'type': 'limit',
|
||||
'price': 2.05,
|
||||
'amount': 100.0,
|
||||
@ -69,7 +106,7 @@ def mock_order_usdt_2_sell():
|
||||
}
|
||||
|
||||
|
||||
def mock_trade_usdt_2(fee):
|
||||
def mock_trade_usdt_2(fee, is_short: bool):
|
||||
"""
|
||||
Closed trade...
|
||||
"""
|
||||
@ -82,30 +119,33 @@ def mock_trade_usdt_2(fee):
|
||||
fee_close=fee.return_value,
|
||||
open_rate=2.0,
|
||||
close_rate=2.05,
|
||||
close_profit=5.0,
|
||||
close_profit=0.05,
|
||||
close_profit_abs=3.9875,
|
||||
exchange='binance',
|
||||
is_open=False,
|
||||
open_order_id='dry_run_sell_12345',
|
||||
open_order_id=f'12366_{direc(is_short)}',
|
||||
strategy='StrategyTestV2',
|
||||
timeframe=5,
|
||||
exit_reason='sell_signal',
|
||||
enter_tag='TEST1',
|
||||
exit_reason='exit_signal',
|
||||
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20),
|
||||
close_date=datetime.now(tz=timezone.utc) - timedelta(minutes=2),
|
||||
is_short=is_short,
|
||||
)
|
||||
o = Order.parse_from_ccxt_object(mock_order_usdt_2(), 'ETC/USDT', 'buy')
|
||||
o = Order.parse_from_ccxt_object(mock_order_usdt_2(is_short), 'ETC/USDT', entry_side(is_short))
|
||||
trade.orders.append(o)
|
||||
o = Order.parse_from_ccxt_object(mock_order_usdt_2_sell(), 'ETC/USDT', 'sell')
|
||||
o = Order.parse_from_ccxt_object(
|
||||
mock_order_usdt_2_exit(is_short), 'ETC/USDT', exit_side(is_short))
|
||||
trade.orders.append(o)
|
||||
return trade
|
||||
|
||||
|
||||
def mock_order_usdt_3():
|
||||
def mock_order_usdt_3(is_short: bool):
|
||||
return {
|
||||
'id': '41231a12a',
|
||||
'id': f'41231a12a_{direc(is_short)}',
|
||||
'symbol': 'XRP/USDT',
|
||||
'status': 'closed',
|
||||
'side': 'buy',
|
||||
'side': entry_side(is_short),
|
||||
'type': 'limit',
|
||||
'price': 1.0,
|
||||
'amount': 30.0,
|
||||
@ -114,12 +154,12 @@ def mock_order_usdt_3():
|
||||
}
|
||||
|
||||
|
||||
def mock_order_usdt_3_sell():
|
||||
def mock_order_usdt_3_exit(is_short: bool):
|
||||
return {
|
||||
'id': '41231a666a',
|
||||
'id': f'41231a666a_{direc(is_short)}',
|
||||
'symbol': 'XRP/USDT',
|
||||
'status': 'closed',
|
||||
'side': 'sell',
|
||||
'side': exit_side(is_short),
|
||||
'type': 'stop_loss_limit',
|
||||
'price': 1.1,
|
||||
'average': 1.1,
|
||||
@ -129,7 +169,7 @@ def mock_order_usdt_3_sell():
|
||||
}
|
||||
|
||||
|
||||
def mock_trade_usdt_3(fee):
|
||||
def mock_trade_usdt_3(fee, is_short: bool):
|
||||
"""
|
||||
Closed trade
|
||||
"""
|
||||
@ -142,29 +182,32 @@ def mock_trade_usdt_3(fee):
|
||||
fee_close=fee.return_value,
|
||||
open_rate=1.0,
|
||||
close_rate=1.1,
|
||||
close_profit=10.0,
|
||||
close_profit=0.1,
|
||||
close_profit_abs=9.8425,
|
||||
exchange='binance',
|
||||
is_open=False,
|
||||
strategy='StrategyTestV2',
|
||||
timeframe=5,
|
||||
enter_tag='TEST3',
|
||||
exit_reason='roi',
|
||||
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20),
|
||||
close_date=datetime.now(tz=timezone.utc),
|
||||
is_short=is_short,
|
||||
)
|
||||
o = Order.parse_from_ccxt_object(mock_order_usdt_3(), 'XRP/USDT', 'buy')
|
||||
o = Order.parse_from_ccxt_object(mock_order_usdt_3(is_short), 'XRP/USDT', entry_side(is_short))
|
||||
trade.orders.append(o)
|
||||
o = Order.parse_from_ccxt_object(mock_order_usdt_3_sell(), 'XRP/USDT', 'sell')
|
||||
o = Order.parse_from_ccxt_object(mock_order_usdt_3_exit(is_short),
|
||||
'XRP/USDT', exit_side(is_short))
|
||||
trade.orders.append(o)
|
||||
return trade
|
||||
|
||||
|
||||
def mock_order_usdt_4():
|
||||
def mock_order_usdt_4(is_short: bool):
|
||||
return {
|
||||
'id': 'prod_buy_12345',
|
||||
'id': f'prod_buy_12345_{direc(is_short)}',
|
||||
'symbol': 'ETC/USDT',
|
||||
'status': 'open',
|
||||
'side': 'buy',
|
||||
'side': entry_side(is_short),
|
||||
'type': 'limit',
|
||||
'price': 2.0,
|
||||
'amount': 10.0,
|
||||
@ -173,7 +216,7 @@ def mock_order_usdt_4():
|
||||
}
|
||||
|
||||
|
||||
def mock_trade_usdt_4(fee):
|
||||
def mock_trade_usdt_4(fee, is_short: bool):
|
||||
"""
|
||||
Simulate prod entry
|
||||
"""
|
||||
@ -188,21 +231,22 @@ def mock_trade_usdt_4(fee):
|
||||
is_open=True,
|
||||
open_rate=2.0,
|
||||
exchange='binance',
|
||||
open_order_id='prod_buy_12345',
|
||||
open_order_id=f'prod_buy_12345_{direc(is_short)}',
|
||||
strategy='StrategyTestV2',
|
||||
timeframe=5,
|
||||
is_short=is_short,
|
||||
)
|
||||
o = Order.parse_from_ccxt_object(mock_order_usdt_4(), 'ETC/USDT', 'buy')
|
||||
o = Order.parse_from_ccxt_object(mock_order_usdt_4(is_short), 'ETC/USDT', entry_side(is_short))
|
||||
trade.orders.append(o)
|
||||
return trade
|
||||
|
||||
|
||||
def mock_order_usdt_5():
|
||||
def mock_order_usdt_5(is_short: bool):
|
||||
return {
|
||||
'id': 'prod_buy_3455',
|
||||
'id': f'prod_buy_3455_{direc(is_short)}',
|
||||
'symbol': 'XRP/USDT',
|
||||
'status': 'closed',
|
||||
'side': 'buy',
|
||||
'side': entry_side(is_short),
|
||||
'type': 'limit',
|
||||
'price': 2.0,
|
||||
'amount': 10.0,
|
||||
@ -211,12 +255,12 @@ def mock_order_usdt_5():
|
||||
}
|
||||
|
||||
|
||||
def mock_order_usdt_5_stoploss():
|
||||
def mock_order_usdt_5_stoploss(is_short: bool):
|
||||
return {
|
||||
'id': 'prod_stoploss_3455',
|
||||
'id': f'prod_stoploss_3455_{direc(is_short)}',
|
||||
'symbol': 'XRP/USDT',
|
||||
'status': 'open',
|
||||
'side': 'sell',
|
||||
'side': exit_side(is_short),
|
||||
'type': 'stop_loss_limit',
|
||||
'price': 2.0,
|
||||
'amount': 10.0,
|
||||
@ -225,7 +269,7 @@ def mock_order_usdt_5_stoploss():
|
||||
}
|
||||
|
||||
|
||||
def mock_trade_usdt_5(fee):
|
||||
def mock_trade_usdt_5(fee, is_short: bool):
|
||||
"""
|
||||
Simulate prod entry with stoploss
|
||||
"""
|
||||
@ -241,22 +285,23 @@ def mock_trade_usdt_5(fee):
|
||||
open_rate=2.0,
|
||||
exchange='binance',
|
||||
strategy='SampleStrategy',
|
||||
stoploss_order_id='prod_stoploss_3455',
|
||||
stoploss_order_id=f'prod_stoploss_3455_{direc(is_short)}',
|
||||
timeframe=5,
|
||||
is_short=is_short,
|
||||
)
|
||||
o = Order.parse_from_ccxt_object(mock_order_usdt_5(), 'XRP/USDT', 'buy')
|
||||
o = Order.parse_from_ccxt_object(mock_order_usdt_5(is_short), 'XRP/USDT', entry_side(is_short))
|
||||
trade.orders.append(o)
|
||||
o = Order.parse_from_ccxt_object(mock_order_usdt_5_stoploss(), 'XRP/USDT', 'stoploss')
|
||||
o = Order.parse_from_ccxt_object(mock_order_usdt_5_stoploss(is_short), 'XRP/USDT', 'stoploss')
|
||||
trade.orders.append(o)
|
||||
return trade
|
||||
|
||||
|
||||
def mock_order_usdt_6():
|
||||
def mock_order_usdt_6(is_short: bool):
|
||||
return {
|
||||
'id': 'prod_buy_6',
|
||||
'id': f'prod_entry_6_{direc(is_short)}',
|
||||
'symbol': 'LTC/USDT',
|
||||
'status': 'closed',
|
||||
'side': 'buy',
|
||||
'side': entry_side(is_short),
|
||||
'type': 'limit',
|
||||
'price': 10.0,
|
||||
'amount': 2.0,
|
||||
@ -265,12 +310,12 @@ def mock_order_usdt_6():
|
||||
}
|
||||
|
||||
|
||||
def mock_order_usdt_6_sell():
|
||||
def mock_order_usdt_6_exit(is_short: bool):
|
||||
return {
|
||||
'id': 'prod_sell_6',
|
||||
'id': f'prod_exit_6_{direc(is_short)}',
|
||||
'symbol': 'LTC/USDT',
|
||||
'status': 'open',
|
||||
'side': 'sell',
|
||||
'side': exit_side(is_short),
|
||||
'type': 'limit',
|
||||
'price': 12.0,
|
||||
'amount': 2.0,
|
||||
@ -279,7 +324,7 @@ def mock_order_usdt_6_sell():
|
||||
}
|
||||
|
||||
|
||||
def mock_trade_usdt_6(fee):
|
||||
def mock_trade_usdt_6(fee, is_short: bool):
|
||||
"""
|
||||
Simulate prod entry with open sell order
|
||||
"""
|
||||
@ -295,69 +340,49 @@ def mock_trade_usdt_6(fee):
|
||||
open_rate=10.0,
|
||||
exchange='binance',
|
||||
strategy='SampleStrategy',
|
||||
open_order_id="prod_sell_6",
|
||||
open_order_id=f'prod_exit_6_{direc(is_short)}',
|
||||
timeframe=5,
|
||||
is_short=is_short,
|
||||
)
|
||||
o = Order.parse_from_ccxt_object(mock_order_usdt_6(), 'LTC/USDT', 'buy')
|
||||
o = Order.parse_from_ccxt_object(mock_order_usdt_6(is_short), 'LTC/USDT', entry_side(is_short))
|
||||
trade.orders.append(o)
|
||||
o = Order.parse_from_ccxt_object(mock_order_usdt_6_sell(), 'LTC/USDT', 'sell')
|
||||
o = Order.parse_from_ccxt_object(mock_order_usdt_6_exit(is_short),
|
||||
'LTC/USDT', exit_side(is_short))
|
||||
trade.orders.append(o)
|
||||
return trade
|
||||
|
||||
|
||||
def mock_order_usdt_7():
|
||||
def mock_order_usdt_7(is_short: bool):
|
||||
return {
|
||||
'id': 'prod_buy_7',
|
||||
'symbol': 'LTC/USDT',
|
||||
'id': f'1234_{direc(is_short)}',
|
||||
'symbol': 'ADA/USDT',
|
||||
'status': 'closed',
|
||||
'side': 'buy',
|
||||
'side': entry_side(is_short),
|
||||
'type': 'limit',
|
||||
'price': 10.0,
|
||||
'amount': 2.0,
|
||||
'filled': 2.0,
|
||||
'price': 2.0,
|
||||
'amount': 10.0,
|
||||
'filled': 10.0,
|
||||
'remaining': 0.0,
|
||||
}
|
||||
|
||||
|
||||
def mock_order_usdt_7_sell():
|
||||
return {
|
||||
'id': 'prod_sell_7',
|
||||
'symbol': 'LTC/USDT',
|
||||
'status': 'closed',
|
||||
'side': 'sell',
|
||||
'type': 'limit',
|
||||
'price': 8.0,
|
||||
'amount': 2.0,
|
||||
'filled': 2.0,
|
||||
'remaining': 0.0,
|
||||
}
|
||||
|
||||
|
||||
def mock_trade_usdt_7(fee):
|
||||
"""
|
||||
Simulate prod entry with open sell order
|
||||
"""
|
||||
def mock_trade_usdt_7(fee, is_short: bool):
|
||||
trade = Trade(
|
||||
pair='LTC/USDT',
|
||||
pair='ADA/USDT',
|
||||
stake_amount=20.0,
|
||||
amount=2.0,
|
||||
amount_requested=2.0,
|
||||
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20),
|
||||
close_date=datetime.now(tz=timezone.utc) - timedelta(minutes=5),
|
||||
amount=10.0,
|
||||
amount_requested=10.0,
|
||||
fee_open=fee.return_value,
|
||||
fee_close=fee.return_value,
|
||||
is_open=False,
|
||||
open_rate=10.0,
|
||||
close_rate=8.0,
|
||||
close_profit=-0.2,
|
||||
close_profit_abs=-4.0,
|
||||
is_open=True,
|
||||
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=17),
|
||||
open_rate=2.0,
|
||||
exchange='binance',
|
||||
strategy='SampleStrategy',
|
||||
open_order_id="prod_sell_6",
|
||||
open_order_id=f'1234_{direc(is_short)}',
|
||||
strategy='StrategyTestV2',
|
||||
timeframe=5,
|
||||
is_short=is_short,
|
||||
)
|
||||
o = Order.parse_from_ccxt_object(mock_order_usdt_7(), 'LTC/USDT', 'buy')
|
||||
trade.orders.append(o)
|
||||
o = Order.parse_from_ccxt_object(mock_order_usdt_7_sell(), 'LTC/USDT', 'sell')
|
||||
o = Order.parse_from_ccxt_object(mock_order_usdt_7(is_short), 'ADA/USDT', entry_side(is_short))
|
||||
trade.orders.append(o)
|
||||
return trade
|
||||
|
@ -85,7 +85,7 @@ def test_load_backtest_data_new_format(testdatadir):
|
||||
filename = testdatadir / "backtest_results/backtest-result_new.json"
|
||||
bt_data = load_backtest_data(filename)
|
||||
assert isinstance(bt_data, DataFrame)
|
||||
assert set(bt_data.columns) == set(BT_DATA_COLUMNS + ['close_timestamp', 'open_timestamp'])
|
||||
assert set(bt_data.columns) == set(BT_DATA_COLUMNS)
|
||||
assert len(bt_data) == 179
|
||||
|
||||
# Test loading from string (must yield same result)
|
||||
@ -110,7 +110,7 @@ def test_load_backtest_data_multi(testdatadir):
|
||||
bt_data = load_backtest_data(filename, strategy=strategy)
|
||||
assert isinstance(bt_data, DataFrame)
|
||||
assert set(bt_data.columns) == set(
|
||||
BT_DATA_COLUMNS + ['close_timestamp', 'open_timestamp'])
|
||||
BT_DATA_COLUMNS)
|
||||
assert len(bt_data) == 179
|
||||
|
||||
# Test loading from string (must yield same result)
|
||||
|
@ -154,6 +154,7 @@ def test_stoploss_adjust_binance(mocker, default_conf, sl1, sl2, sl3, side):
|
||||
order = {
|
||||
'type': 'stop_loss_limit',
|
||||
'price': 1500,
|
||||
'stopPrice': 1500,
|
||||
'info': {'stopPrice': 1500},
|
||||
}
|
||||
assert exchange.stoploss_adjust(sl1, order, side=side)
|
||||
|
@ -795,10 +795,27 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None:
|
||||
'is_open': [False, False],
|
||||
'enter_tag': [None, None],
|
||||
"is_short": [False, False],
|
||||
'open_timestamp': [1517251200000, 1517283000000],
|
||||
'close_timestamp': [1517265300000, 1517285400000],
|
||||
'orders': [
|
||||
[
|
||||
{'amount': 0.00957442, 'safe_price': 0.104445, 'ft_order_side': 'buy',
|
||||
'order_filled_timestamp': 1517251200000, 'ft_is_entry': True},
|
||||
{'amount': 0.00957442, 'safe_price': 0.10496853383458644, 'ft_order_side': 'sell',
|
||||
'order_filled_timestamp': 1517265300000, 'ft_is_entry': False}
|
||||
], [
|
||||
{'amount': 0.0097064, 'safe_price': 0.10302485, 'ft_order_side': 'buy',
|
||||
'order_filled_timestamp': 1517283000000, 'ft_is_entry': True},
|
||||
{'amount': 0.0097064, 'safe_price': 0.10354126528822055, 'ft_order_side': 'sell',
|
||||
'order_filled_timestamp': 1517285400000, 'ft_is_entry': False}
|
||||
]
|
||||
]
|
||||
})
|
||||
pd.testing.assert_frame_equal(results, expected)
|
||||
assert 'orders' in results.columns
|
||||
data_pair = processed[pair]
|
||||
for _, t in results.iterrows():
|
||||
assert len(t['orders']) == 2
|
||||
ln = data_pair.loc[data_pair["date"] == t["open_date"]]
|
||||
# Check open trade rate alignes to open rate
|
||||
assert ln is not None
|
||||
|
@ -72,9 +72,14 @@ def test_backtest_position_adjustment(default_conf, fee, mocker, testdatadir) ->
|
||||
'is_open': [False, False],
|
||||
'enter_tag': [None, None],
|
||||
'is_short': [False, False],
|
||||
'open_timestamp': [1517251200000, 1517283000000],
|
||||
'close_timestamp': [1517265300000, 1517285400000],
|
||||
})
|
||||
pd.testing.assert_frame_equal(results, expected)
|
||||
pd.testing.assert_frame_equal(results.drop(columns=['orders']), expected)
|
||||
data_pair = processed[pair]
|
||||
assert len(results.iloc[0]['orders']) == 6
|
||||
assert len(results.iloc[1]['orders']) == 2
|
||||
|
||||
for _, t in results.iterrows():
|
||||
ln = data_pair.loc[data_pair["date"] == t["open_date"]]
|
||||
# Check open trade rate alignes to open rate
|
||||
|
@ -509,7 +509,6 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None:
|
||||
hyperopt.min_date = Arrow(2017, 12, 10)
|
||||
hyperopt.max_date = Arrow(2017, 12, 13)
|
||||
hyperopt.init_spaces()
|
||||
hyperopt.dimensions = hyperopt.dimensions
|
||||
generate_optimizer_value = hyperopt.generate_optimizer(list(optimizer_param.values()))
|
||||
assert generate_optimizer_value == response_expected
|
||||
|
||||
|
@ -762,8 +762,8 @@ def test_PerformanceFilter_keep_mid_order(mocker, default_conf_usdt, fee, caplog
|
||||
with time_machine.travel("2021-09-01 05:00:00 +00:00") as t:
|
||||
create_mock_trades_usdt(fee)
|
||||
pm.refresh_pairlist()
|
||||
assert pm.whitelist == ['XRP/USDT', 'ETC/USDT', 'ETH/USDT',
|
||||
'NEO/USDT', 'TKN/USDT', 'ADA/USDT', 'LTC/USDT']
|
||||
assert pm.whitelist == ['XRP/USDT', 'ETC/USDT', 'ETH/USDT', 'LTC/USDT',
|
||||
'NEO/USDT', 'TKN/USDT', 'ADA/USDT', ]
|
||||
# assert log_has_re(r'Removing pair .* since .* is below .*', caplog)
|
||||
|
||||
# Move to "outside" of lookback window, so original sorting is restored.
|
||||
|
@ -11,11 +11,11 @@ from freqtrade.edge import PairInfo
|
||||
from freqtrade.enums import SignalDirection, State, TradingMode
|
||||
from freqtrade.exceptions import ExchangeError, InvalidOrderException, TemporaryError
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.persistence.models import Order
|
||||
from freqtrade.persistence.pairlock_middleware import PairLocks
|
||||
from freqtrade.rpc import RPC, RPCException
|
||||
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
|
||||
from tests.conftest import create_mock_trades, get_patched_freqtradebot, patch_get_signal
|
||||
from tests.conftest import (create_mock_trades, create_mock_trades_usdt, get_patched_freqtradebot,
|
||||
patch_get_signal)
|
||||
|
||||
|
||||
# Functions for recurrent object patching
|
||||
@ -286,7 +286,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None:
|
||||
assert isnan(fiat_profit_sum)
|
||||
|
||||
|
||||
def test_rpc_daily_profit(default_conf, update, ticker, fee,
|
||||
def test__rpc_timeunit_profit(default_conf_usdt, ticker, fee,
|
||||
limit_buy_order, limit_sell_order, markets, mocker) -> None:
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
mocker.patch.multiple(
|
||||
@ -296,45 +296,35 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee,
|
||||
markets=PropertyMock(return_value=markets)
|
||||
)
|
||||
|
||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||
patch_get_signal(freqtradebot)
|
||||
stake_currency = default_conf['stake_currency']
|
||||
fiat_display_currency = default_conf['fiat_display_currency']
|
||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||
create_mock_trades_usdt(fee)
|
||||
|
||||
stake_currency = default_conf_usdt['stake_currency']
|
||||
fiat_display_currency = default_conf_usdt['fiat_display_currency']
|
||||
|
||||
rpc = RPC(freqtradebot)
|
||||
rpc._fiat_converter = CryptoToFiatConverter()
|
||||
# Create some test data
|
||||
freqtradebot.enter_positions()
|
||||
trade = Trade.query.first()
|
||||
assert trade
|
||||
|
||||
# Simulate buy & sell
|
||||
oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
|
||||
trade.update_trade(oobj)
|
||||
oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
|
||||
trade.update_trade(oobj)
|
||||
trade.close_date = datetime.utcnow()
|
||||
trade.is_open = False
|
||||
|
||||
# Try valid data
|
||||
update.message.text = '/daily 2'
|
||||
days = rpc._rpc_daily_profit(7, stake_currency, fiat_display_currency)
|
||||
days = rpc._rpc_timeunit_profit(7, stake_currency, fiat_display_currency)
|
||||
assert len(days['data']) == 7
|
||||
assert days['stake_currency'] == default_conf['stake_currency']
|
||||
assert days['fiat_display_currency'] == default_conf['fiat_display_currency']
|
||||
assert days['stake_currency'] == default_conf_usdt['stake_currency']
|
||||
assert days['fiat_display_currency'] == default_conf_usdt['fiat_display_currency']
|
||||
for day in days['data']:
|
||||
# [datetime.date(2018, 1, 11), '0.00000000 BTC', '0.000 USD']
|
||||
assert (day['abs_profit'] == 0.0 or
|
||||
day['abs_profit'] == 0.00006217)
|
||||
|
||||
assert (day['fiat_value'] == 0.0 or
|
||||
day['fiat_value'] == 0.76748865)
|
||||
# {'date': datetime.date(2022, 6, 11), 'abs_profit': 13.8299999,
|
||||
# 'starting_balance': 1055.37, 'rel_profit': 0.0131044,
|
||||
# 'fiat_value': 0.0, 'trade_count': 2}
|
||||
assert day['abs_profit'] in (0.0, pytest.approx(13.8299999), pytest.approx(-4.0))
|
||||
assert day['rel_profit'] in (0.0, pytest.approx(0.01310441), pytest.approx(-0.00377583))
|
||||
assert day['trade_count'] in (0, 1, 2)
|
||||
assert day['starting_balance'] in (pytest.approx(1059.37), pytest.approx(1055.37))
|
||||
assert day['fiat_value'] in (0.0, )
|
||||
# ensure first day is current date
|
||||
assert str(days['data'][0]['date']) == str(datetime.utcnow().date())
|
||||
|
||||
# Try invalid data
|
||||
with pytest.raises(RPCException, match=r'.*must be an integer greater than 0*'):
|
||||
rpc._rpc_daily_profit(0, stake_currency, fiat_display_currency)
|
||||
rpc._rpc_timeunit_profit(0, stake_currency, fiat_display_currency)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('is_short', [True, False])
|
||||
@ -418,13 +408,8 @@ def test_rpc_delete_trade(mocker, default_conf, fee, markets, caplog, is_short):
|
||||
assert stoploss_mock.call_count == 0
|
||||
|
||||
|
||||
def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
|
||||
limit_buy_order, limit_sell_order, mocker) -> None:
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.rpc.fiat_convert.CoinGeckoAPI',
|
||||
get_price=MagicMock(return_value={'bitcoin': {'usd': 15000.0}}),
|
||||
)
|
||||
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
|
||||
def test_rpc_trade_statistics(default_conf_usdt, ticker, fee, mocker) -> None:
|
||||
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=1.1)
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
@ -432,10 +417,9 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
|
||||
get_fee=fee,
|
||||
)
|
||||
|
||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||
patch_get_signal(freqtradebot)
|
||||
stake_currency = default_conf['stake_currency']
|
||||
fiat_display_currency = default_conf['fiat_display_currency']
|
||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||
stake_currency = default_conf_usdt['stake_currency']
|
||||
fiat_display_currency = default_conf_usdt['fiat_display_currency']
|
||||
|
||||
rpc = RPC(freqtradebot)
|
||||
rpc._fiat_converter = CryptoToFiatConverter()
|
||||
@ -448,75 +432,40 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
|
||||
assert res['latest_trade_timestamp'] == 0
|
||||
|
||||
# Create some test data
|
||||
freqtradebot.enter_positions()
|
||||
trade = Trade.query.first()
|
||||
# Simulate fulfilled LIMIT_BUY order for trade
|
||||
oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
|
||||
trade.update_trade(oobj)
|
||||
|
||||
# Update the ticker with a market going up
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
fetch_ticker=ticker_sell_up
|
||||
)
|
||||
oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
|
||||
trade.update_trade(oobj)
|
||||
trade.close_date = datetime.utcnow()
|
||||
trade.is_open = False
|
||||
|
||||
freqtradebot.enter_positions()
|
||||
trade = Trade.query.first()
|
||||
# Simulate fulfilled LIMIT_BUY order for trade
|
||||
oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
|
||||
trade.update_trade(oobj)
|
||||
|
||||
# Update the ticker with a market going up
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
fetch_ticker=ticker_sell_up
|
||||
)
|
||||
oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
|
||||
trade.update_trade(oobj)
|
||||
trade.close_date = datetime.utcnow()
|
||||
trade.is_open = False
|
||||
create_mock_trades_usdt(fee)
|
||||
|
||||
stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
|
||||
assert prec_satoshi(stats['profit_closed_coin'], 6.217e-05)
|
||||
assert prec_satoshi(stats['profit_closed_percent_mean'], 6.2)
|
||||
assert prec_satoshi(stats['profit_closed_fiat'], 0.93255)
|
||||
assert prec_satoshi(stats['profit_all_coin'], 5.802e-05)
|
||||
assert prec_satoshi(stats['profit_all_percent_mean'], 2.89)
|
||||
assert prec_satoshi(stats['profit_all_fiat'], 0.8703)
|
||||
assert stats['trade_count'] == 2
|
||||
assert stats['first_trade_date'] == 'just now'
|
||||
assert stats['latest_trade_date'] == 'just now'
|
||||
assert stats['avg_duration'] in ('0:00:00', '0:00:01', '0:00:02')
|
||||
assert stats['best_pair'] == 'ETH/BTC'
|
||||
assert prec_satoshi(stats['best_rate'], 6.2)
|
||||
assert pytest.approx(stats['profit_closed_coin']) == 9.83
|
||||
assert pytest.approx(stats['profit_closed_percent_mean']) == -1.67
|
||||
assert pytest.approx(stats['profit_closed_fiat']) == 10.813
|
||||
assert pytest.approx(stats['profit_all_coin']) == -77.45964918
|
||||
assert pytest.approx(stats['profit_all_percent_mean']) == -57.86
|
||||
assert pytest.approx(stats['profit_all_fiat']) == -85.205614098
|
||||
assert stats['trade_count'] == 7
|
||||
assert stats['first_trade_date'] == '2 days ago'
|
||||
assert stats['latest_trade_date'] == '17 minutes ago'
|
||||
assert stats['avg_duration'] in ('0:17:40')
|
||||
assert stats['best_pair'] == 'XRP/USDT'
|
||||
assert stats['best_rate'] == 10.0
|
||||
|
||||
# Test non-available pair
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_rate',
|
||||
MagicMock(side_effect=ExchangeError("Pair 'ETH/BTC' not available")))
|
||||
MagicMock(side_effect=ExchangeError("Pair 'XRP/USDT' not available")))
|
||||
stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
|
||||
assert stats['trade_count'] == 2
|
||||
assert stats['first_trade_date'] == 'just now'
|
||||
assert stats['latest_trade_date'] == 'just now'
|
||||
assert stats['avg_duration'] in ('0:00:00', '0:00:01', '0:00:02')
|
||||
assert stats['best_pair'] == 'ETH/BTC'
|
||||
assert prec_satoshi(stats['best_rate'], 6.2)
|
||||
assert stats['trade_count'] == 7
|
||||
assert stats['first_trade_date'] == '2 days ago'
|
||||
assert stats['latest_trade_date'] == '17 minutes ago'
|
||||
assert stats['avg_duration'] in ('0:17:40')
|
||||
assert stats['best_pair'] == 'XRP/USDT'
|
||||
assert stats['best_rate'] == 10.0
|
||||
assert isnan(stats['profit_all_coin'])
|
||||
|
||||
|
||||
# Test that rpc_trade_statistics can handle trades that lacks
|
||||
# trade.open_rate (it is set to None)
|
||||
def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee,
|
||||
ticker_sell_up, limit_buy_order, limit_sell_order):
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.rpc.fiat_convert.CoinGeckoAPI',
|
||||
get_price=MagicMock(return_value={'bitcoin': {'usd': 15000.0}}),
|
||||
)
|
||||
def test_rpc_trade_statistics_closed(mocker, default_conf_usdt, ticker, fee):
|
||||
mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price',
|
||||
return_value=15000.0)
|
||||
return_value=1.1)
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
@ -524,46 +473,32 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee,
|
||||
get_fee=fee,
|
||||
)
|
||||
|
||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||
patch_get_signal(freqtradebot)
|
||||
stake_currency = default_conf['stake_currency']
|
||||
fiat_display_currency = default_conf['fiat_display_currency']
|
||||
stake_currency = default_conf_usdt['stake_currency']
|
||||
fiat_display_currency = default_conf_usdt['fiat_display_currency']
|
||||
|
||||
rpc = RPC(freqtradebot)
|
||||
|
||||
# Create some test data
|
||||
freqtradebot.enter_positions()
|
||||
trade = Trade.query.first()
|
||||
# Simulate fulfilled LIMIT_BUY order for trade
|
||||
oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
|
||||
trade.update_trade(oobj)
|
||||
# Update the ticker with a market going up
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
fetch_ticker=ticker_sell_up,
|
||||
get_fee=fee
|
||||
)
|
||||
oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
|
||||
trade.update_trade(oobj)
|
||||
trade.close_date = datetime.utcnow()
|
||||
trade.is_open = False
|
||||
create_mock_trades_usdt(fee)
|
||||
|
||||
for trade in Trade.query.order_by(Trade.id).all():
|
||||
trade.open_rate = None
|
||||
|
||||
stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
|
||||
assert prec_satoshi(stats['profit_closed_coin'], 0)
|
||||
assert prec_satoshi(stats['profit_closed_percent_mean'], 0)
|
||||
assert prec_satoshi(stats['profit_closed_fiat'], 0)
|
||||
assert prec_satoshi(stats['profit_all_coin'], 0)
|
||||
assert prec_satoshi(stats['profit_all_percent_mean'], 0)
|
||||
assert prec_satoshi(stats['profit_all_fiat'], 0)
|
||||
assert stats['trade_count'] == 1
|
||||
assert stats['first_trade_date'] == 'just now'
|
||||
assert stats['latest_trade_date'] == 'just now'
|
||||
assert stats['profit_closed_coin'] == 0
|
||||
assert stats['profit_closed_percent_mean'] == 0
|
||||
assert stats['profit_closed_fiat'] == 0
|
||||
assert stats['profit_all_coin'] == 0
|
||||
assert stats['profit_all_percent_mean'] == 0
|
||||
assert stats['profit_all_fiat'] == 0
|
||||
assert stats['trade_count'] == 7
|
||||
assert stats['first_trade_date'] == '2 days ago'
|
||||
assert stats['latest_trade_date'] == '17 minutes ago'
|
||||
assert stats['avg_duration'] == '0:00:00'
|
||||
assert stats['best_pair'] == 'ETH/BTC'
|
||||
assert prec_satoshi(stats['best_rate'], 6.2)
|
||||
assert stats['best_pair'] == 'XRP/USDT'
|
||||
assert stats['best_rate'] == 10.0
|
||||
|
||||
|
||||
def test_rpc_balance_handle_error(default_conf, mocker):
|
||||
@ -916,8 +851,7 @@ def test_rpc_force_exit(default_conf, ticker, fee, mocker) -> None:
|
||||
assert cancel_order_mock.call_count == 3
|
||||
|
||||
|
||||
def test_performance_handle(default_conf, ticker, limit_buy_order, fee,
|
||||
limit_sell_order, mocker) -> None:
|
||||
def test_performance_handle(default_conf_usdt, ticker, fee, mocker) -> None:
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
@ -926,34 +860,21 @@ def test_performance_handle(default_conf, ticker, limit_buy_order, fee,
|
||||
get_fee=fee,
|
||||
)
|
||||
|
||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||
patch_get_signal(freqtradebot)
|
||||
rpc = RPC(freqtradebot)
|
||||
|
||||
# Create some test data
|
||||
freqtradebot.enter_positions()
|
||||
trade = Trade.query.first()
|
||||
assert trade
|
||||
create_mock_trades_usdt(fee)
|
||||
|
||||
# Simulate fulfilled LIMIT_BUY order for trade
|
||||
oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
|
||||
trade.update_trade(oobj)
|
||||
|
||||
# Simulate fulfilled LIMIT_SELL order for trade
|
||||
oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
|
||||
trade.update_trade(oobj)
|
||||
|
||||
trade.close_date = datetime.utcnow()
|
||||
trade.is_open = False
|
||||
res = rpc._rpc_performance()
|
||||
assert len(res) == 1
|
||||
assert res[0]['pair'] == 'ETH/BTC'
|
||||
assert len(res) == 3
|
||||
assert res[0]['pair'] == 'XRP/USDT'
|
||||
assert res[0]['count'] == 1
|
||||
assert prec_satoshi(res[0]['profit_pct'], 6.2)
|
||||
assert res[0]['profit_pct'] == 10.0
|
||||
|
||||
|
||||
def test_enter_tag_performance_handle(default_conf, ticker, limit_buy_order, fee,
|
||||
limit_sell_order, mocker) -> None:
|
||||
def test_enter_tag_performance_handle(default_conf, ticker, fee, mocker) -> None:
|
||||
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
@ -967,34 +888,22 @@ def test_enter_tag_performance_handle(default_conf, ticker, limit_buy_order, fee
|
||||
rpc = RPC(freqtradebot)
|
||||
|
||||
# Create some test data
|
||||
create_mock_trades_usdt(fee)
|
||||
freqtradebot.enter_positions()
|
||||
trade = Trade.query.first()
|
||||
assert trade
|
||||
|
||||
# Simulate fulfilled LIMIT_BUY order for trade
|
||||
oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
|
||||
trade.update_trade(oobj)
|
||||
|
||||
# Simulate fulfilled LIMIT_SELL order for trade
|
||||
oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
|
||||
trade.update_trade(oobj)
|
||||
|
||||
trade.close_date = datetime.utcnow()
|
||||
trade.is_open = False
|
||||
res = rpc._rpc_enter_tag_performance(None)
|
||||
|
||||
assert len(res) == 1
|
||||
assert res[0]['enter_tag'] == 'Other'
|
||||
assert len(res) == 3
|
||||
assert res[0]['enter_tag'] == 'TEST3'
|
||||
assert res[0]['count'] == 1
|
||||
assert prec_satoshi(res[0]['profit_pct'], 6.2)
|
||||
assert res[0]['profit_pct'] == 10.0
|
||||
|
||||
trade.enter_tag = "TEST_TAG"
|
||||
res = rpc._rpc_enter_tag_performance(None)
|
||||
|
||||
assert len(res) == 1
|
||||
assert res[0]['enter_tag'] == 'TEST_TAG'
|
||||
assert len(res) == 3
|
||||
assert res[0]['enter_tag'] == 'TEST3'
|
||||
assert res[0]['count'] == 1
|
||||
assert prec_satoshi(res[0]['profit_pct'], 6.2)
|
||||
assert res[0]['profit_pct'] == 10.0
|
||||
|
||||
|
||||
def test_enter_tag_performance_handle_2(mocker, default_conf, markets, fee):
|
||||
@ -1026,8 +935,7 @@ def test_enter_tag_performance_handle_2(mocker, default_conf, markets, fee):
|
||||
assert prec_satoshi(res[0]['profit_pct'], 0.5)
|
||||
|
||||
|
||||
def test_exit_reason_performance_handle(default_conf, ticker, limit_buy_order, fee,
|
||||
limit_sell_order, mocker) -> None:
|
||||
def test_exit_reason_performance_handle(default_conf_usdt, ticker, fee, mocker) -> None:
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
@ -1036,39 +944,22 @@ def test_exit_reason_performance_handle(default_conf, ticker, limit_buy_order, f
|
||||
get_fee=fee,
|
||||
)
|
||||
|
||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||
patch_get_signal(freqtradebot)
|
||||
rpc = RPC(freqtradebot)
|
||||
|
||||
# Create some test data
|
||||
freqtradebot.enter_positions()
|
||||
trade = Trade.query.first()
|
||||
assert trade
|
||||
create_mock_trades_usdt(fee)
|
||||
|
||||
# Simulate fulfilled LIMIT_BUY order for trade
|
||||
oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
|
||||
trade.update_trade(oobj)
|
||||
|
||||
# Simulate fulfilled LIMIT_SELL order for trade
|
||||
oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
|
||||
trade.update_trade(oobj)
|
||||
|
||||
trade.close_date = datetime.utcnow()
|
||||
trade.is_open = False
|
||||
res = rpc._rpc_exit_reason_performance(None)
|
||||
|
||||
assert len(res) == 1
|
||||
assert res[0]['exit_reason'] == 'Other'
|
||||
assert len(res) == 3
|
||||
assert res[0]['exit_reason'] == 'roi'
|
||||
assert res[0]['count'] == 1
|
||||
assert prec_satoshi(res[0]['profit_pct'], 6.2)
|
||||
assert res[0]['profit_pct'] == 10.0
|
||||
|
||||
trade.exit_reason = "TEST1"
|
||||
res = rpc._rpc_exit_reason_performance(None)
|
||||
|
||||
assert len(res) == 1
|
||||
assert res[0]['exit_reason'] == 'TEST1'
|
||||
assert res[0]['count'] == 1
|
||||
assert prec_satoshi(res[0]['profit_pct'], 6.2)
|
||||
assert res[1]['exit_reason'] == 'exit_signal'
|
||||
assert res[2]['exit_reason'] == 'Other'
|
||||
|
||||
|
||||
def test_exit_reason_performance_handle_2(mocker, default_conf, markets, fee):
|
||||
@ -1100,8 +991,7 @@ def test_exit_reason_performance_handle_2(mocker, default_conf, markets, fee):
|
||||
assert prec_satoshi(res[0]['profit_pct'], 0.5)
|
||||
|
||||
|
||||
def test_mix_tag_performance_handle(default_conf, ticker, limit_buy_order, fee,
|
||||
limit_sell_order, mocker) -> None:
|
||||
def test_mix_tag_performance_handle(default_conf, ticker, fee, mocker) -> None:
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
@ -1115,35 +1005,14 @@ def test_mix_tag_performance_handle(default_conf, ticker, limit_buy_order, fee,
|
||||
rpc = RPC(freqtradebot)
|
||||
|
||||
# Create some test data
|
||||
freqtradebot.enter_positions()
|
||||
trade = Trade.query.first()
|
||||
assert trade
|
||||
create_mock_trades_usdt(fee)
|
||||
|
||||
# Simulate fulfilled LIMIT_BUY order for trade
|
||||
oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
|
||||
trade.update_trade(oobj)
|
||||
|
||||
# Simulate fulfilled LIMIT_SELL order for trade
|
||||
oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
|
||||
trade.update_trade(oobj)
|
||||
|
||||
trade.close_date = datetime.utcnow()
|
||||
trade.is_open = False
|
||||
res = rpc._rpc_mix_tag_performance(None)
|
||||
|
||||
assert len(res) == 1
|
||||
assert res[0]['mix_tag'] == 'Other Other'
|
||||
assert len(res) == 3
|
||||
assert res[0]['mix_tag'] == 'TEST3 roi'
|
||||
assert res[0]['count'] == 1
|
||||
assert prec_satoshi(res[0]['profit_pct'], 6.2)
|
||||
|
||||
trade.enter_tag = "TESTBUY"
|
||||
trade.exit_reason = "TESTSELL"
|
||||
res = rpc._rpc_mix_tag_performance(None)
|
||||
|
||||
assert len(res) == 1
|
||||
assert res[0]['mix_tag'] == 'TESTBUY TESTSELL'
|
||||
assert res[0]['count'] == 1
|
||||
assert prec_satoshi(res[0]['profit_pct'], 6.2)
|
||||
assert res[0]['profit_pct'] == 10.0
|
||||
|
||||
|
||||
def test_mix_tag_performance_handle_2(mocker, default_conf, markets, fee):
|
||||
|
@ -27,8 +27,9 @@ from freqtrade.persistence.models import Order
|
||||
from freqtrade.rpc import RPC
|
||||
from freqtrade.rpc.rpc import RPCException
|
||||
from freqtrade.rpc.telegram import Telegram, authorized_only
|
||||
from tests.conftest import (CURRENT_TEST_STRATEGY, create_mock_trades, get_patched_freqtradebot,
|
||||
log_has, log_has_re, patch_exchange, patch_get_signal, patch_whitelist)
|
||||
from tests.conftest import (CURRENT_TEST_STRATEGY, create_mock_trades, create_mock_trades_usdt,
|
||||
get_patched_freqtradebot, log_has, log_has_re, patch_exchange,
|
||||
patch_get_signal, patch_whitelist)
|
||||
|
||||
|
||||
class DummyCls(Telegram):
|
||||
@ -404,12 +405,10 @@ def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None:
|
||||
assert msg_mock.call_count == 1
|
||||
|
||||
|
||||
def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee,
|
||||
limit_sell_order, mocker) -> None:
|
||||
default_conf['max_open_trades'] = 1
|
||||
def test_daily_handle(default_conf_usdt, update, ticker, fee, mocker, time_machine) -> None:
|
||||
mocker.patch(
|
||||
'freqtrade.rpc.rpc.CryptoToFiatConverter._find_price',
|
||||
return_value=15000.0
|
||||
return_value=1.1
|
||||
)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
@ -417,27 +416,12 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee,
|
||||
get_fee=fee,
|
||||
)
|
||||
|
||||
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
|
||||
|
||||
patch_get_signal(freqtradebot)
|
||||
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf_usdt)
|
||||
|
||||
# Move date to within day
|
||||
time_machine.move_to('2022-06-11 08:00:00+00:00')
|
||||
# Create some test data
|
||||
freqtradebot.enter_positions()
|
||||
trade = Trade.query.first()
|
||||
assert trade
|
||||
|
||||
# Simulate fulfilled LIMIT_BUY order for trade
|
||||
limit_buy_order['id'] = trade.orders[0].order_id
|
||||
oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
|
||||
trade.update_order(limit_buy_order)
|
||||
trade.update_trade(oobj)
|
||||
|
||||
# Simulate fulfilled LIMIT_SELL order for trade
|
||||
oobjs = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
|
||||
trade.update_trade(oobjs)
|
||||
|
||||
trade.close_date = datetime.utcnow()
|
||||
trade.is_open = False
|
||||
create_mock_trades_usdt(fee)
|
||||
|
||||
# Try valid data
|
||||
# /daily 2
|
||||
@ -448,10 +432,11 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee,
|
||||
assert "Daily Profit over the last 2 days</b>:" in msg_mock.call_args_list[0][0][0]
|
||||
assert 'Day ' in msg_mock.call_args_list[0][0][0]
|
||||
assert str(datetime.utcnow().date()) in msg_mock.call_args_list[0][0][0]
|
||||
assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0]
|
||||
assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0]
|
||||
assert str(' 1 trade') in msg_mock.call_args_list[0][0][0]
|
||||
assert str(' 0 trade') in msg_mock.call_args_list[0][0][0]
|
||||
assert ' 13.83 USDT' in msg_mock.call_args_list[0][0][0]
|
||||
assert ' 15.21 USD' in msg_mock.call_args_list[0][0][0]
|
||||
assert '(2)' in msg_mock.call_args_list[0][0][0]
|
||||
assert '(2) 13.83 USDT 15.21 USD 1.31%' in msg_mock.call_args_list[0][0][0]
|
||||
assert '(0)' in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
# Reset msg_mock
|
||||
msg_mock.reset_mock()
|
||||
@ -460,32 +445,23 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee,
|
||||
assert msg_mock.call_count == 1
|
||||
assert "Daily Profit over the last 7 days</b>:" in msg_mock.call_args_list[0][0][0]
|
||||
assert str(datetime.utcnow().date()) in msg_mock.call_args_list[0][0][0]
|
||||
assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0]
|
||||
assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0]
|
||||
assert str(' 1 trade') in msg_mock.call_args_list[0][0][0]
|
||||
assert str(' 0 trade') in msg_mock.call_args_list[0][0][0]
|
||||
assert str((datetime.utcnow() - timedelta(days=5)).date()) in msg_mock.call_args_list[0][0][0]
|
||||
assert ' 13.83 USDT' in msg_mock.call_args_list[0][0][0]
|
||||
assert ' 15.21 USD' in msg_mock.call_args_list[0][0][0]
|
||||
assert '(2)' in msg_mock.call_args_list[0][0][0]
|
||||
assert '(1)' in msg_mock.call_args_list[0][0][0]
|
||||
assert '(0)' in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
# Reset msg_mock
|
||||
msg_mock.reset_mock()
|
||||
freqtradebot.config['max_open_trades'] = 2
|
||||
# Add two other trades
|
||||
n = freqtradebot.enter_positions()
|
||||
assert n == 2
|
||||
|
||||
trades = Trade.query.all()
|
||||
for trade in trades:
|
||||
trade.update_trade(oobj)
|
||||
trade.update_trade(oobjs)
|
||||
trade.close_date = datetime.utcnow()
|
||||
trade.is_open = False
|
||||
|
||||
# /daily 1
|
||||
context = MagicMock()
|
||||
context.args = ["1"]
|
||||
telegram._daily(update=update, context=context)
|
||||
assert str(' 0.00018651 BTC') in msg_mock.call_args_list[0][0][0]
|
||||
assert str(' 2.798 USD') in msg_mock.call_args_list[0][0][0]
|
||||
assert str(' 3 trades') in msg_mock.call_args_list[0][0][0]
|
||||
assert ' 13.83 USDT' in msg_mock.call_args_list[0][0][0]
|
||||
assert ' 15.21 USD' in msg_mock.call_args_list[0][0][0]
|
||||
assert '(2)' in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
|
||||
def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None:
|
||||
@ -514,15 +490,14 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None:
|
||||
context = MagicMock()
|
||||
context.args = ["today"]
|
||||
telegram._daily(update=update, context=context)
|
||||
assert str('Daily Profit over the last 7 days</b>:') in msg_mock.call_args_list[0][0][0]
|
||||
assert 'Daily Profit over the last 7 days</b>:' in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
|
||||
def test_weekly_handle(default_conf, update, ticker, limit_buy_order, fee,
|
||||
limit_sell_order, mocker) -> None:
|
||||
default_conf['max_open_trades'] = 1
|
||||
def test_weekly_handle(default_conf_usdt, update, ticker, fee, mocker, time_machine) -> None:
|
||||
default_conf_usdt['max_open_trades'] = 1
|
||||
mocker.patch(
|
||||
'freqtrade.rpc.rpc.CryptoToFiatConverter._find_price',
|
||||
return_value=15000.0
|
||||
return_value=1.1
|
||||
)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
@ -530,27 +505,10 @@ def test_weekly_handle(default_conf, update, ticker, limit_buy_order, fee,
|
||||
get_fee=fee,
|
||||
)
|
||||
|
||||
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
|
||||
|
||||
patch_get_signal(freqtradebot)
|
||||
|
||||
# Create some test data
|
||||
freqtradebot.enter_positions()
|
||||
trade = Trade.query.first()
|
||||
assert trade
|
||||
|
||||
# Simulate fulfilled LIMIT_BUY order for trade
|
||||
limit_buy_order['id'] = trade.orders[0].order_id
|
||||
oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
|
||||
trade.update_order(limit_buy_order)
|
||||
trade.update_trade(oobj)
|
||||
|
||||
# Simulate fulfilled LIMIT_SELL order for trade
|
||||
oobjs = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
|
||||
trade.update_trade(oobjs)
|
||||
|
||||
trade.close_date = datetime.utcnow()
|
||||
trade.is_open = False
|
||||
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf_usdt)
|
||||
# Move to saturday - so all trades are within that week
|
||||
time_machine.move_to('2022-06-11')
|
||||
create_mock_trades_usdt(fee)
|
||||
|
||||
# Try valid data
|
||||
# /weekly 2
|
||||
@ -564,10 +522,10 @@ def test_weekly_handle(default_conf, update, ticker, limit_buy_order, fee,
|
||||
today = datetime.utcnow().date()
|
||||
first_iso_day_of_current_week = today - timedelta(days=today.weekday())
|
||||
assert str(first_iso_day_of_current_week) in msg_mock.call_args_list[0][0][0]
|
||||
assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0]
|
||||
assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0]
|
||||
assert str(' 1 trade') in msg_mock.call_args_list[0][0][0]
|
||||
assert str(' 0 trade') in msg_mock.call_args_list[0][0][0]
|
||||
assert ' 9.83 USDT' in msg_mock.call_args_list[0][0][0]
|
||||
assert ' 10.81 USD' in msg_mock.call_args_list[0][0][0]
|
||||
assert '(3)' in msg_mock.call_args_list[0][0][0]
|
||||
assert '(0)' in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
# Reset msg_mock
|
||||
msg_mock.reset_mock()
|
||||
@ -577,44 +535,10 @@ def test_weekly_handle(default_conf, update, ticker, limit_buy_order, fee,
|
||||
assert "Weekly Profit over the last 8 weeks (starting from Monday)</b>:" \
|
||||
in msg_mock.call_args_list[0][0][0]
|
||||
assert 'Weekly' in msg_mock.call_args_list[0][0][0]
|
||||
assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0]
|
||||
assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0]
|
||||
assert str(' 1 trade') in msg_mock.call_args_list[0][0][0]
|
||||
assert str(' 0 trade') in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
# Reset msg_mock
|
||||
msg_mock.reset_mock()
|
||||
freqtradebot.config['max_open_trades'] = 2
|
||||
# Add two other trades
|
||||
n = freqtradebot.enter_positions()
|
||||
assert n == 2
|
||||
|
||||
trades = Trade.query.all()
|
||||
for trade in trades:
|
||||
trade.update_trade(oobj)
|
||||
trade.update_trade(oobjs)
|
||||
trade.close_date = datetime.utcnow()
|
||||
trade.is_open = False
|
||||
|
||||
# /weekly 1
|
||||
# By default, the 8 previous weeks are shown
|
||||
# So the previous modified trade should be excluded from the stats
|
||||
context = MagicMock()
|
||||
context.args = ["1"]
|
||||
telegram._weekly(update=update, context=context)
|
||||
assert str(' 0.00018651 BTC') in msg_mock.call_args_list[0][0][0]
|
||||
assert str(' 2.798 USD') in msg_mock.call_args_list[0][0][0]
|
||||
assert str(' 3 trades') in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
|
||||
def test_weekly_wrong_input(default_conf, update, ticker, mocker) -> None:
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
fetch_ticker=ticker
|
||||
)
|
||||
|
||||
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
|
||||
patch_get_signal(freqtradebot)
|
||||
assert ' 9.83 USDT' in msg_mock.call_args_list[0][0][0]
|
||||
assert ' 10.81 USD' in msg_mock.call_args_list[0][0][0]
|
||||
assert '(3)' in msg_mock.call_args_list[0][0][0]
|
||||
assert '(0)' in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
# Try invalid data
|
||||
msg_mock.reset_mock()
|
||||
@ -633,16 +557,17 @@ def test_weekly_wrong_input(default_conf, update, ticker, mocker) -> None:
|
||||
context = MagicMock()
|
||||
context.args = ["this week"]
|
||||
telegram._weekly(update=update, context=context)
|
||||
assert str('Weekly Profit over the last 8 weeks (starting from Monday)</b>:') \
|
||||
assert (
|
||||
'Weekly Profit over the last 8 weeks (starting from Monday)</b>:'
|
||||
in msg_mock.call_args_list[0][0][0]
|
||||
)
|
||||
|
||||
|
||||
def test_monthly_handle(default_conf, update, ticker, limit_buy_order, fee,
|
||||
limit_sell_order, mocker) -> None:
|
||||
default_conf['max_open_trades'] = 1
|
||||
def test_monthly_handle(default_conf_usdt, update, ticker, fee, mocker, time_machine) -> None:
|
||||
default_conf_usdt['max_open_trades'] = 1
|
||||
mocker.patch(
|
||||
'freqtrade.rpc.rpc.CryptoToFiatConverter._find_price',
|
||||
return_value=15000.0
|
||||
return_value=1.1
|
||||
)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
@ -650,27 +575,10 @@ def test_monthly_handle(default_conf, update, ticker, limit_buy_order, fee,
|
||||
get_fee=fee,
|
||||
)
|
||||
|
||||
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
|
||||
|
||||
patch_get_signal(freqtradebot)
|
||||
|
||||
# Create some test data
|
||||
freqtradebot.enter_positions()
|
||||
trade = Trade.query.first()
|
||||
assert trade
|
||||
|
||||
# Simulate fulfilled LIMIT_BUY order for trade
|
||||
limit_buy_order['id'] = trade.orders[0].order_id
|
||||
oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
|
||||
trade.update_order(limit_buy_order)
|
||||
trade.update_trade(oobj)
|
||||
|
||||
# Simulate fulfilled LIMIT_SELL order for trade
|
||||
oobjs = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
|
||||
trade.update_trade(oobjs)
|
||||
|
||||
trade.close_date = datetime.utcnow()
|
||||
trade.is_open = False
|
||||
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf_usdt)
|
||||
# Move to day within the month so all mock trades fall into this week.
|
||||
time_machine.move_to('2022-06-11')
|
||||
create_mock_trades_usdt(fee)
|
||||
|
||||
# Try valid data
|
||||
# /monthly 2
|
||||
@ -683,10 +591,10 @@ def test_monthly_handle(default_conf, update, ticker, limit_buy_order, fee,
|
||||
today = datetime.utcnow().date()
|
||||
current_month = f"{today.year}-{today.month:02} "
|
||||
assert current_month in msg_mock.call_args_list[0][0][0]
|
||||
assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0]
|
||||
assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0]
|
||||
assert str(' 1 trade') in msg_mock.call_args_list[0][0][0]
|
||||
assert str(' 0 trade') in msg_mock.call_args_list[0][0][0]
|
||||
assert ' 9.83 USDT' in msg_mock.call_args_list[0][0][0]
|
||||
assert ' 10.81 USD' in msg_mock.call_args_list[0][0][0]
|
||||
assert '(3)' in msg_mock.call_args_list[0][0][0]
|
||||
assert '(0)' in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
# Reset msg_mock
|
||||
msg_mock.reset_mock()
|
||||
@ -697,24 +605,13 @@ def test_monthly_handle(default_conf, update, ticker, limit_buy_order, fee,
|
||||
assert 'Monthly Profit over the last 6 months</b>:' in msg_mock.call_args_list[0][0][0]
|
||||
assert 'Month ' in msg_mock.call_args_list[0][0][0]
|
||||
assert current_month in msg_mock.call_args_list[0][0][0]
|
||||
assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0]
|
||||
assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0]
|
||||
assert str(' 1 trade') in msg_mock.call_args_list[0][0][0]
|
||||
assert str(' 0 trade') in msg_mock.call_args_list[0][0][0]
|
||||
assert ' 9.83 USDT' in msg_mock.call_args_list[0][0][0]
|
||||
assert ' 10.81 USD' in msg_mock.call_args_list[0][0][0]
|
||||
assert '(3)' in msg_mock.call_args_list[0][0][0]
|
||||
assert '(0)' in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
# Reset msg_mock
|
||||
msg_mock.reset_mock()
|
||||
freqtradebot.config['max_open_trades'] = 2
|
||||
# Add two other trades
|
||||
n = freqtradebot.enter_positions()
|
||||
assert n == 2
|
||||
|
||||
trades = Trade.query.all()
|
||||
for trade in trades:
|
||||
trade.update_trade(oobj)
|
||||
trade.update_trade(oobjs)
|
||||
trade.close_date = datetime.utcnow()
|
||||
trade.is_open = False
|
||||
|
||||
# /monthly 12
|
||||
context = MagicMock()
|
||||
@ -722,24 +619,14 @@ def test_monthly_handle(default_conf, update, ticker, limit_buy_order, fee,
|
||||
telegram._monthly(update=update, context=context)
|
||||
assert msg_mock.call_count == 1
|
||||
assert 'Monthly Profit over the last 12 months</b>:' in msg_mock.call_args_list[0][0][0]
|
||||
assert str(' 0.00018651 BTC') in msg_mock.call_args_list[0][0][0]
|
||||
assert str(' 2.798 USD') in msg_mock.call_args_list[0][0][0]
|
||||
assert str(' 3 trades') in msg_mock.call_args_list[0][0][0]
|
||||
assert ' 9.83 USDT' in msg_mock.call_args_list[0][0][0]
|
||||
assert ' 10.81 USD' in msg_mock.call_args_list[0][0][0]
|
||||
assert '(3)' in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
# The one-digit months should contain a zero, Eg: September 2021 = "2021-09"
|
||||
# Since we loaded the last 12 months, any month should appear
|
||||
assert str('-09') in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
|
||||
def test_monthly_wrong_input(default_conf, update, ticker, mocker) -> None:
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
fetch_ticker=ticker
|
||||
)
|
||||
|
||||
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
|
||||
patch_get_signal(freqtradebot)
|
||||
|
||||
# Try invalid data
|
||||
msg_mock.reset_mock()
|
||||
freqtradebot.state = State.RUNNING
|
||||
@ -760,16 +647,16 @@ def test_monthly_wrong_input(default_conf, update, ticker, mocker) -> None:
|
||||
assert str('Monthly Profit over the last 6 months</b>:') in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
|
||||
def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
|
||||
limit_buy_order, limit_sell_order, mocker) -> None:
|
||||
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
|
||||
def test_profit_handle(default_conf_usdt, update, ticker_usdt, ticker_sell_up, fee,
|
||||
limit_sell_order_usdt, mocker) -> None:
|
||||
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=1.1)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
fetch_ticker=ticker,
|
||||
fetch_ticker=ticker_usdt,
|
||||
get_fee=fee,
|
||||
)
|
||||
|
||||
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
|
||||
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf_usdt)
|
||||
patch_get_signal(freqtradebot)
|
||||
|
||||
telegram._profit(update=update, context=MagicMock())
|
||||
@ -781,13 +668,6 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
|
||||
freqtradebot.enter_positions()
|
||||
trade = Trade.query.first()
|
||||
|
||||
# Simulate fulfilled LIMIT_BUY order for trade
|
||||
limit_buy_order['id'] = trade.orders[0].order_id
|
||||
oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
|
||||
trade.update_order(limit_buy_order)
|
||||
trade.update_trade(oobj)
|
||||
Trade.commit()
|
||||
|
||||
context = MagicMock()
|
||||
# Test with invalid 2nd argument (should silently pass)
|
||||
context.args = ["aaa"]
|
||||
@ -795,15 +675,16 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
|
||||
assert msg_mock.call_count == 1
|
||||
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]
|
||||
mocker.patch('freqtrade.wallets.Wallets.get_starting_balance', return_value=0.01)
|
||||
assert ('∙ `-0.000005 BTC (-0.50%) (-0.0 \N{GREEK CAPITAL LETTER SIGMA}%)`'
|
||||
mocker.patch('freqtrade.wallets.Wallets.get_starting_balance', return_value=1000)
|
||||
assert ('∙ `0.298 USDT (0.50%) (0.03 \N{GREEK CAPITAL LETTER SIGMA}%)`'
|
||||
in msg_mock.call_args_list[-1][0][0])
|
||||
msg_mock.reset_mock()
|
||||
|
||||
# Update the ticker with a market going up
|
||||
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', ticker_sell_up)
|
||||
# Simulate fulfilled LIMIT_SELL order for trade
|
||||
oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
|
||||
oobj = Order.parse_from_ccxt_object(
|
||||
limit_sell_order_usdt, limit_sell_order_usdt['symbol'], 'sell')
|
||||
trade.update_trade(oobj)
|
||||
|
||||
trade.close_date = datetime.now(timezone.utc)
|
||||
@ -814,15 +695,15 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
|
||||
telegram._profit(update=update, context=context)
|
||||
assert msg_mock.call_count == 1
|
||||
assert '*ROI:* Closed trades' in msg_mock.call_args_list[-1][0][0]
|
||||
assert ('∙ `0.00006217 BTC (6.20%) (0.62 \N{GREEK CAPITAL LETTER SIGMA}%)`'
|
||||
assert ('∙ `5.685 USDT (9.45%) (0.57 \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 '∙ `6.253 USD`' 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%) (0.62 \N{GREEK CAPITAL LETTER SIGMA}%)`'
|
||||
assert ('∙ `5.685 USDT (9.45%) (0.57 \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 '∙ `6.253 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/USDT: 9.45%`' in msg_mock.call_args_list[-1][0][0]
|
||||
|
||||
|
||||
@pytest.mark.parametrize('is_short', [True, False])
|
||||
@ -1365,75 +1246,43 @@ def test_force_enter_no_pair(default_conf, update, mocker) -> None:
|
||||
assert fbuy_mock.call_count == 1
|
||||
|
||||
|
||||
def test_telegram_performance_handle(default_conf, update, ticker, fee,
|
||||
limit_buy_order, limit_sell_order, mocker) -> None:
|
||||
def test_telegram_performance_handle(default_conf_usdt, update, ticker, fee, mocker) -> None:
|
||||
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
fetch_ticker=ticker,
|
||||
get_fee=fee,
|
||||
)
|
||||
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
|
||||
patch_get_signal(freqtradebot)
|
||||
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf_usdt)
|
||||
|
||||
# Create some test data
|
||||
freqtradebot.enter_positions()
|
||||
trade = Trade.query.first()
|
||||
assert trade
|
||||
create_mock_trades_usdt(fee)
|
||||
|
||||
# Simulate fulfilled LIMIT_BUY order for trade
|
||||
limit_buy_order['id'] = trade.orders[0].order_id
|
||||
oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
|
||||
trade.update_order(limit_buy_order)
|
||||
trade.update_trade(oobj)
|
||||
|
||||
# Simulate fulfilled LIMIT_SELL order for trade
|
||||
oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
|
||||
trade.update_trade(oobj)
|
||||
|
||||
trade.close_date = datetime.utcnow()
|
||||
trade.is_open = False
|
||||
telegram._performance(update=update, context=MagicMock())
|
||||
assert msg_mock.call_count == 1
|
||||
assert 'Performance' in msg_mock.call_args_list[0][0][0]
|
||||
assert '<code>ETH/BTC\t0.00006217 BTC (6.20%) (1)</code>' in msg_mock.call_args_list[0][0][0]
|
||||
assert '<code>XRP/USDT\t9.842 USDT (10.00%) (1)</code>' in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
|
||||
def test_telegram_entry_tag_performance_handle(
|
||||
default_conf, update, ticker, fee, limit_buy_order, limit_sell_order, mocker) -> None:
|
||||
default_conf_usdt, update, ticker, fee, mocker) -> None:
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
fetch_ticker=ticker,
|
||||
get_fee=fee,
|
||||
)
|
||||
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
|
||||
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf_usdt)
|
||||
patch_get_signal(freqtradebot)
|
||||
|
||||
# Create some test data
|
||||
freqtradebot.enter_positions()
|
||||
trade = Trade.query.first()
|
||||
assert trade
|
||||
create_mock_trades_usdt(fee)
|
||||
|
||||
# Simulate fulfilled LIMIT_BUY order for trade
|
||||
limit_buy_order['id'] = trade.orders[0].order_id
|
||||
oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
|
||||
trade.update_order(limit_buy_order)
|
||||
trade.update_trade(oobj)
|
||||
|
||||
trade.enter_tag = "TESTBUY"
|
||||
# Simulate fulfilled LIMIT_SELL order for trade
|
||||
oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
|
||||
trade.update_trade(oobj)
|
||||
|
||||
trade.close_date = datetime.utcnow()
|
||||
trade.is_open = False
|
||||
context = MagicMock()
|
||||
telegram._enter_tag_performance(update=update, context=context)
|
||||
assert msg_mock.call_count == 1
|
||||
assert 'Entry Tag Performance' in msg_mock.call_args_list[0][0][0]
|
||||
assert '<code>TESTBUY\t0.00006217 BTC (6.20%) (1)</code>' in msg_mock.call_args_list[0][0][0]
|
||||
assert '<code>TEST1\t3.987 USDT (5.00%) (1)</code>' in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
context.args = [trade.pair]
|
||||
context.args = ['XRP/USDT']
|
||||
telegram._enter_tag_performance(update=update, context=context)
|
||||
assert msg_mock.call_count == 2
|
||||
|
||||
@ -1446,39 +1295,24 @@ def test_telegram_entry_tag_performance_handle(
|
||||
assert "Error" in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
|
||||
def test_telegram_exit_reason_performance_handle(default_conf, update, ticker, fee,
|
||||
limit_buy_order, limit_sell_order, mocker) -> None:
|
||||
def test_telegram_exit_reason_performance_handle(default_conf_usdt, update, ticker, fee,
|
||||
mocker) -> None:
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
fetch_ticker=ticker,
|
||||
get_fee=fee,
|
||||
)
|
||||
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
|
||||
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf_usdt)
|
||||
patch_get_signal(freqtradebot)
|
||||
|
||||
# Create some test data
|
||||
freqtradebot.enter_positions()
|
||||
trade = Trade.query.first()
|
||||
assert trade
|
||||
trade.exit_reason = 'TESTSELL'
|
||||
# Simulate fulfilled LIMIT_BUY order for trade
|
||||
limit_buy_order['id'] = trade.orders[0].order_id
|
||||
oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
|
||||
trade.update_order(limit_buy_order)
|
||||
trade.update_trade(oobj)
|
||||
create_mock_trades_usdt(fee)
|
||||
|
||||
# Simulate fulfilled LIMIT_SELL order for trade
|
||||
oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
|
||||
trade.update_trade(oobj)
|
||||
|
||||
trade.close_date = datetime.utcnow()
|
||||
trade.is_open = False
|
||||
context = MagicMock()
|
||||
telegram._exit_reason_performance(update=update, context=context)
|
||||
assert msg_mock.call_count == 1
|
||||
assert 'Exit Reason Performance' in msg_mock.call_args_list[0][0][0]
|
||||
assert '<code>TESTSELL\t0.00006217 BTC (6.20%) (1)</code>' in msg_mock.call_args_list[0][0][0]
|
||||
context.args = [trade.pair]
|
||||
assert '<code>roi\t9.842 USDT (10.00%) (1)</code>' in msg_mock.call_args_list[0][0][0]
|
||||
context.args = ['XRP/USDT']
|
||||
|
||||
telegram._exit_reason_performance(update=update, context=context)
|
||||
assert msg_mock.call_count == 2
|
||||
@ -1492,45 +1326,27 @@ def test_telegram_exit_reason_performance_handle(default_conf, update, ticker, f
|
||||
assert "Error" in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
|
||||
def test_telegram_mix_tag_performance_handle(default_conf, update, ticker, fee,
|
||||
limit_buy_order, limit_sell_order, mocker) -> None:
|
||||
def test_telegram_mix_tag_performance_handle(default_conf_usdt, update, ticker, fee,
|
||||
mocker) -> None:
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
fetch_ticker=ticker,
|
||||
get_fee=fee,
|
||||
)
|
||||
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
|
||||
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf_usdt)
|
||||
patch_get_signal(freqtradebot)
|
||||
|
||||
# Create some test data
|
||||
freqtradebot.enter_positions()
|
||||
trade = Trade.query.first()
|
||||
assert trade
|
||||
|
||||
trade.enter_tag = "TESTBUY"
|
||||
trade.exit_reason = "TESTSELL"
|
||||
|
||||
# Simulate fulfilled LIMIT_BUY order for trade
|
||||
limit_buy_order['id'] = trade.orders[0].order_id
|
||||
oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
|
||||
trade.update_order(limit_buy_order)
|
||||
trade.update_trade(oobj)
|
||||
|
||||
# Simulate fulfilled LIMIT_SELL order for trade
|
||||
oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
|
||||
trade.update_trade(oobj)
|
||||
|
||||
trade.close_date = datetime.utcnow()
|
||||
trade.is_open = False
|
||||
create_mock_trades_usdt(fee)
|
||||
|
||||
context = MagicMock()
|
||||
telegram._mix_tag_performance(update=update, context=context)
|
||||
assert msg_mock.call_count == 1
|
||||
assert 'Mix Tag Performance' in msg_mock.call_args_list[0][0][0]
|
||||
assert ('<code>TESTBUY TESTSELL\t0.00006217 BTC (6.20%) (1)</code>'
|
||||
assert ('<code>TEST3 roi\t9.842 USDT (10.00%) (1)</code>'
|
||||
in msg_mock.call_args_list[0][0][0])
|
||||
|
||||
context.args = [trade.pair]
|
||||
context.args = ['XRP/USDT']
|
||||
telegram._mix_tag_performance(update=update, context=context)
|
||||
assert msg_mock.call_count == 2
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
# pragma pylint: disable=missing-docstring, C0103, protected-access
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
@ -7,6 +8,7 @@ from requests import RequestException
|
||||
|
||||
from freqtrade.enums import ExitType, RPCMessageType
|
||||
from freqtrade.rpc import RPC
|
||||
from freqtrade.rpc.discord import Discord
|
||||
from freqtrade.rpc.webhook import Webhook
|
||||
from tests.conftest import get_patched_freqtradebot, log_has
|
||||
|
||||
@ -406,3 +408,42 @@ def test__send_msg_with_raw_format(default_conf, mocker, caplog):
|
||||
webhook._send_msg(msg)
|
||||
|
||||
assert post.call_args[1] == {'data': msg['data'], 'headers': {'Content-Type': 'text/plain'}}
|
||||
|
||||
|
||||
def test_send_msg_discord(default_conf, mocker):
|
||||
|
||||
default_conf["discord"] = {
|
||||
'enabled': True,
|
||||
'webhook_url': "https://webhookurl..."
|
||||
}
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock)
|
||||
discord = Discord(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf)
|
||||
|
||||
msg = {
|
||||
'type': RPCMessageType.EXIT_FILL,
|
||||
'trade_id': 1,
|
||||
'exchange': 'Binance',
|
||||
'pair': 'ETH/BTC',
|
||||
'direction': 'Long',
|
||||
'gain': "profit",
|
||||
'close_rate': 0.005,
|
||||
'amount': 0.8,
|
||||
'order_type': 'limit',
|
||||
'open_date': datetime.now() - timedelta(days=1),
|
||||
'close_date': datetime.now(),
|
||||
'open_rate': 0.004,
|
||||
'current_rate': 0.005,
|
||||
'profit_amount': 0.001,
|
||||
'profit_ratio': 0.20,
|
||||
'stake_currency': 'BTC',
|
||||
'enter_tag': 'enter_tagggg',
|
||||
'exit_reason': ExitType.STOP_LOSS.value,
|
||||
}
|
||||
discord.send_msg(msg=msg)
|
||||
|
||||
assert msg_mock.call_count == 1
|
||||
assert 'embeds' in msg_mock.call_args_list[0][0][0]
|
||||
assert 'title' in msg_mock.call_args_list[0][0][0]['embeds'][0]
|
||||
assert 'color' in msg_mock.call_args_list[0][0][0]['embeds'][0]
|
||||
assert 'fields' in msg_mock.call_args_list[0][0][0]['embeds'][0]
|
||||
|
@ -27,7 +27,6 @@ class HyperoptableStrategy(StrategyTestV2):
|
||||
'sell_minusdi': 0.4
|
||||
}
|
||||
|
||||
buy_rsi = IntParameter([0, 50], default=30, space='buy')
|
||||
buy_plusdi = RealParameter(low=0, high=1, default=0.5, space='buy')
|
||||
sell_rsi = IntParameter(low=50, high=100, default=70, space='sell')
|
||||
sell_minusdi = DecimalParameter(low=0, high=1, default=0.5001, decimals=3, space='sell',
|
||||
@ -45,6 +44,12 @@ class HyperoptableStrategy(StrategyTestV2):
|
||||
})
|
||||
return prot
|
||||
|
||||
def bot_start(self, **kwargs) -> None:
|
||||
"""
|
||||
Parameters can also be defined here ...
|
||||
"""
|
||||
self.buy_rsi = IntParameter([0, 50], default=30, space='buy')
|
||||
|
||||
def informative_pairs(self):
|
||||
"""
|
||||
Define additional, informative pair/interval combinations to be cached from the exchange.
|
||||
|
@ -178,8 +178,8 @@ class StrategyTestV3(IStrategy):
|
||||
return dataframe
|
||||
|
||||
def leverage(self, pair: str, current_time: datetime, current_rate: float,
|
||||
proposed_leverage: float, max_leverage: float, side: str,
|
||||
**kwargs) -> float:
|
||||
proposed_leverage: float, max_leverage: float, entry_tag: Optional[str],
|
||||
side: str, **kwargs) -> float:
|
||||
# Return 3.0 in all cases.
|
||||
# Bot-logic must make sure it's an allowed leverage and eventually adjust accordingly.
|
||||
|
||||
|
@ -16,10 +16,12 @@ from freqtrade.exceptions import OperationalException, StrategyError
|
||||
from freqtrade.optimize.space import SKDecimal
|
||||
from freqtrade.persistence import PairLocks, Trade
|
||||
from freqtrade.resolvers import StrategyResolver
|
||||
from freqtrade.strategy.hyper import detect_parameters
|
||||
from freqtrade.strategy.parameters import (BaseParameter, BooleanParameter, CategoricalParameter,
|
||||
DecimalParameter, IntParameter, RealParameter)
|
||||
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
|
||||
from tests.conftest import CURRENT_TEST_STRATEGY, TRADE_SIDES, log_has, log_has_re
|
||||
from tests.conftest import (CURRENT_TEST_STRATEGY, TRADE_SIDES, create_mock_trades, log_has,
|
||||
log_has_re)
|
||||
|
||||
from .strats.strategy_test_v3 import StrategyTestV3
|
||||
|
||||
@ -614,6 +616,7 @@ def test_leverage_callback(default_conf, side) -> None:
|
||||
proposed_leverage=1.0,
|
||||
max_leverage=5.0,
|
||||
side=side,
|
||||
entry_tag=None,
|
||||
) == 1
|
||||
|
||||
default_conf['strategy'] = CURRENT_TEST_STRATEGY
|
||||
@ -625,6 +628,7 @@ def test_leverage_callback(default_conf, side) -> None:
|
||||
proposed_leverage=1.0,
|
||||
max_leverage=5.0,
|
||||
side=side,
|
||||
entry_tag='entry_tag_test',
|
||||
) == 3
|
||||
|
||||
|
||||
@ -809,6 +813,28 @@ def test_strategy_safe_wrapper(value):
|
||||
assert ret == value
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_persistence")
|
||||
def test_strategy_safe_wrapper_trade_copy(fee):
|
||||
create_mock_trades(fee)
|
||||
|
||||
def working_method(trade):
|
||||
assert len(trade.orders) > 0
|
||||
assert trade.orders
|
||||
trade.orders = []
|
||||
assert len(trade.orders) == 0
|
||||
return trade
|
||||
|
||||
trade = Trade.get_open_trades()[0]
|
||||
# Don't assert anything before strategy_wrapper.
|
||||
# This ensures that relationship loading works correctly.
|
||||
ret = strategy_safe_wrapper(working_method, message='DeadBeef')(trade=trade)
|
||||
assert isinstance(ret, Trade)
|
||||
assert id(trade) != id(ret)
|
||||
# Did not modify the original order
|
||||
assert len(trade.orders) > 0
|
||||
assert len(ret.orders) == 0
|
||||
|
||||
|
||||
def test_hyperopt_parameters():
|
||||
from skopt.space import Categorical, Integer, Real
|
||||
with pytest.raises(OperationalException, match=r"Name is determined.*"):
|
||||
@ -893,7 +919,7 @@ def test_auto_hyperopt_interface(default_conf):
|
||||
default_conf.update({'strategy': 'HyperoptableStrategy'})
|
||||
PairLocks.timeframe = default_conf['timeframe']
|
||||
strategy = StrategyResolver.load_strategy(default_conf)
|
||||
|
||||
strategy.ft_bot_start()
|
||||
with pytest.raises(OperationalException):
|
||||
next(strategy.enumerate_parameters('deadBeef'))
|
||||
|
||||
@ -908,15 +934,18 @@ def test_auto_hyperopt_interface(default_conf):
|
||||
assert strategy.sell_minusdi.value == 0.5
|
||||
all_params = strategy.detect_all_parameters()
|
||||
assert isinstance(all_params, dict)
|
||||
assert len(all_params['buy']) == 2
|
||||
# Only one buy param at class level
|
||||
assert len(all_params['buy']) == 1
|
||||
# Running detect params at instance level reveals both parameters.
|
||||
assert len(list(detect_parameters(strategy, 'buy'))) == 2
|
||||
assert len(all_params['sell']) == 2
|
||||
# Number of Hyperoptable parameters
|
||||
assert all_params['count'] == 6
|
||||
assert all_params['count'] == 5
|
||||
|
||||
strategy.__class__.sell_rsi = IntParameter([0, 10], default=5, space='buy')
|
||||
|
||||
with pytest.raises(OperationalException, match=r"Inconclusive parameter.*"):
|
||||
[x for x in strategy.detect_parameters('sell')]
|
||||
[x for x in detect_parameters(strategy, 'sell')]
|
||||
|
||||
|
||||
def test_auto_hyperopt_interface_loadparams(default_conf, mocker, caplog):
|
||||
|
@ -209,13 +209,14 @@ def test_edge_overrides_stoploss(limit_order, fee, caplog, mocker,
|
||||
#
|
||||
# mocking the ticker: price is falling ...
|
||||
enter_price = limit_order['buy']['price']
|
||||
ticker_val = {
|
||||
'bid': enter_price,
|
||||
'ask': enter_price,
|
||||
'last': enter_price,
|
||||
}
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
fetch_ticker=MagicMock(return_value={
|
||||
'bid': enter_price * buy_price_mult,
|
||||
'ask': enter_price * buy_price_mult,
|
||||
'last': enter_price * buy_price_mult,
|
||||
}),
|
||||
fetch_ticker=MagicMock(return_value=ticker_val),
|
||||
get_fee=fee,
|
||||
)
|
||||
#############################################
|
||||
@ -228,11 +229,12 @@ def test_edge_overrides_stoploss(limit_order, fee, caplog, mocker,
|
||||
freqtrade.enter_positions()
|
||||
trade = Trade.query.first()
|
||||
caplog.clear()
|
||||
limit_order['buy']['id'] = trade.orders[0].order_id
|
||||
oobj = Order.parse_from_ccxt_object(limit_order['buy'], 'NEO/BTC', 'buy')
|
||||
trade.update_order(limit_order['buy'])
|
||||
trade.update_trade(oobj)
|
||||
#############################################
|
||||
ticker_val.update({
|
||||
'bid': enter_price * buy_price_mult,
|
||||
'ask': enter_price * buy_price_mult,
|
||||
'last': enter_price * buy_price_mult,
|
||||
})
|
||||
|
||||
# stoploss shoud be hit
|
||||
assert freqtrade.handle_trade(trade) is not ignore_strat_sl
|
||||
@ -1776,9 +1778,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog,
|
||||
'type': 'stop_loss_limit',
|
||||
'price': 3,
|
||||
'average': 2,
|
||||
'info': {
|
||||
'stopPrice': '2.178'
|
||||
}
|
||||
})
|
||||
|
||||
mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_order_hanging)
|
||||
@ -2577,6 +2577,7 @@ def test_check_handle_cancelled_buy(
|
||||
get_fee=fee
|
||||
)
|
||||
freqtrade = FreqtradeBot(default_conf_usdt)
|
||||
open_trade.orders = []
|
||||
open_trade.is_short = is_short
|
||||
Trade.query.session.add(open_trade)
|
||||
|
||||
@ -2961,6 +2962,7 @@ def test_handle_cancel_enter(mocker, caplog, default_conf_usdt, limit_order, is_
|
||||
freqtrade = FreqtradeBot(default_conf_usdt)
|
||||
freqtrade._notify_enter_cancel = MagicMock()
|
||||
|
||||
# TODO: Convert to real trade
|
||||
trade = MagicMock()
|
||||
trade.pair = 'LTC/USDT'
|
||||
trade.open_rate = 200
|
||||
@ -2968,6 +2970,7 @@ def test_handle_cancel_enter(mocker, caplog, default_conf_usdt, limit_order, is_
|
||||
trade.entry_side = "buy"
|
||||
l_order['filled'] = 0.0
|
||||
l_order['status'] = 'open'
|
||||
trade.nr_of_successful_entries = 0
|
||||
reason = CANCEL_REASON['TIMEOUT']
|
||||
assert freqtrade.handle_cancel_enter(trade, l_order, reason)
|
||||
assert cancel_order_mock.call_count == 1
|
||||
@ -3010,7 +3013,9 @@ def test_handle_cancel_enter_exchanges(mocker, caplog, default_conf_usdt, is_sho
|
||||
freqtrade = FreqtradeBot(default_conf_usdt)
|
||||
|
||||
reason = CANCEL_REASON['TIMEOUT']
|
||||
# TODO: Convert to real trade
|
||||
trade = MagicMock()
|
||||
trade.nr_of_successful_entries = 0
|
||||
trade.pair = 'LTC/ETH'
|
||||
trade.entry_side = "sell" if is_short else "buy"
|
||||
assert freqtrade.handle_cancel_enter(trade, limit_buy_order_canceled_empty, reason)
|
||||
@ -3043,13 +3048,14 @@ def test_handle_cancel_enter_corder_empty(mocker, default_conf_usdt, limit_order
|
||||
|
||||
freqtrade = FreqtradeBot(default_conf_usdt)
|
||||
freqtrade._notify_enter_cancel = MagicMock()
|
||||
|
||||
# TODO: Convert to real trade
|
||||
trade = MagicMock()
|
||||
trade.pair = 'LTC/USDT'
|
||||
trade.entry_side = "buy"
|
||||
trade.open_rate = 200
|
||||
trade.entry_side = "buy"
|
||||
trade.open_order_id = "open_order_noop"
|
||||
trade.nr_of_successful_entries = 0
|
||||
l_order['filled'] = 0.0
|
||||
l_order['status'] = 'open'
|
||||
reason = CANCEL_REASON['TIMEOUT']
|
||||
@ -3783,6 +3789,7 @@ def test_exit_profit_only(
|
||||
trade = Trade.query.first()
|
||||
assert trade.is_short == is_short
|
||||
oobj = Order.parse_from_ccxt_object(limit_order[eside], limit_order[eside]['symbol'], eside)
|
||||
trade.update_order(limit_order[eside])
|
||||
trade.update_trade(oobj)
|
||||
freqtrade.wallets.update()
|
||||
if profit_only:
|
||||
@ -4071,6 +4078,7 @@ def test_trailing_stop_loss_positive(
|
||||
trade = Trade.query.first()
|
||||
assert trade.is_short == is_short
|
||||
oobj = Order.parse_from_ccxt_object(limit_order[eside], limit_order[eside]['symbol'], eside)
|
||||
trade.update_order(limit_order[eside])
|
||||
trade.update_trade(oobj)
|
||||
caplog.set_level(logging.DEBUG)
|
||||
# stop-loss not reached
|
||||
@ -4816,10 +4824,19 @@ def test_startup_update_open_orders(mocker, default_conf_usdt, fee, caplog, is_s
|
||||
assert len(Order.get_open_orders()) == 2
|
||||
|
||||
caplog.clear()
|
||||
mocker.patch('freqtrade.exchange.Exchange.fetch_order', side_effect=InvalidOrderException)
|
||||
mocker.patch('freqtrade.exchange.Exchange.fetch_order', side_effect=ExchangeError)
|
||||
freqtrade.startup_update_open_orders()
|
||||
assert log_has_re(r"Error updating Order .*", caplog)
|
||||
|
||||
mocker.patch('freqtrade.exchange.Exchange.fetch_order', side_effect=InvalidOrderException)
|
||||
hto_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_timedout_order')
|
||||
# Orders which are no longer found after X days should be assumed as canceled.
|
||||
freqtrade.startup_update_open_orders()
|
||||
assert log_has_re(r"Order is older than \d days.*", caplog)
|
||||
assert hto_mock.call_count == 2
|
||||
assert hto_mock.call_args_list[0][0][0]['status'] == 'canceled'
|
||||
assert hto_mock.call_args_list[1][0][0]['status'] == 'canceled'
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_persistence")
|
||||
@pytest.mark.parametrize("is_short", [False, True])
|
||||
|
Loading…
Reference in New Issue
Block a user