mirror of
https://github.com/freqtrade/freqtrade.git
synced 2024-11-14 04:03:55 +00:00
Merge branch 'develop' into feature_keyval_storage
This commit is contained in:
commit
4c6074062c
12
.github/workflows/ci.yml
vendored
12
.github/workflows/ci.yml
vendored
|
@ -30,7 +30,7 @@ jobs:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v3
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
|
@ -127,7 +127,7 @@ jobs:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v3
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
|
@ -211,7 +211,7 @@ jobs:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v3
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
|
@ -263,7 +263,7 @@ jobs:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v3
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: "3.10"
|
python-version: "3.10"
|
||||||
|
|
||||||
|
@ -282,7 +282,7 @@ jobs:
|
||||||
./tests/test_docs.sh
|
./tests/test_docs.sh
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v3
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: "3.10"
|
python-version: "3.10"
|
||||||
|
|
||||||
|
@ -336,7 +336,7 @@ jobs:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v3
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: "3.9"
|
python-version: "3.9"
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ repos:
|
||||||
exclude: build_helpers
|
exclude: build_helpers
|
||||||
additional_dependencies:
|
additional_dependencies:
|
||||||
- types-cachetools==5.0.1
|
- types-cachetools==5.0.1
|
||||||
- types-filelock==3.2.6
|
- types-filelock==3.2.7
|
||||||
- types-requests==2.27.30
|
- types-requests==2.27.30
|
||||||
- types-tabulate==0.8.9
|
- types-tabulate==0.8.9
|
||||||
- types-python-dateutil==2.8.17
|
- types-python-dateutil==2.8.17
|
||||||
|
|
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 |
|
@ -1,5 +1,5 @@
|
||||||
mkdocs==1.3.0
|
mkdocs==1.3.0
|
||||||
mkdocs-material==8.3.2
|
mkdocs-material==8.3.4
|
||||||
mdx_truly_sane_lists==1.2
|
mdx_truly_sane_lists==1.2
|
||||||
pymdown-extensions==9.4
|
pymdown-extensions==9.5
|
||||||
jinja2==3.1.2
|
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.
|
If you'd still like to remove a trade from the database directly, you can use the below query.
|
||||||
|
|
||||||
```sql
|
!!! Danger
|
||||||
DELETE FROM trades WHERE id = <tradeid>;
|
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
|
```sql
|
||||||
|
DELETE FROM trades WHERE id = <tradeid>;
|
||||||
|
|
||||||
DELETE FROM trades WHERE id = 31;
|
DELETE FROM trades WHERE id = 31;
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -102,13 +103,20 @@ DELETE FROM trades WHERE id = 31;
|
||||||
|
|
||||||
## Use a different database system
|
## 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
|
!!! 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
|
### PostgreSQL
|
||||||
|
|
||||||
Freqtrade supports PostgreSQL by using SQLAlchemy, which supports multiple different database systems.
|
|
||||||
|
|
||||||
Installation:
|
Installation:
|
||||||
`pip install psycopg2-binary`
|
`pip install psycopg2-binary`
|
||||||
|
|
||||||
|
|
|
@ -551,6 +551,7 @@ class AwesomeStrategy(IStrategy):
|
||||||
:param order_type: Order type (as configured in order_types). usually limit or market.
|
:param order_type: Order type (as configured in order_types). usually limit or market.
|
||||||
:param amount: Amount in target (base) 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
|
: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 time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
|
||||||
:param current_time: datetime object, containing the current datetime
|
:param current_time: datetime object, containing the current datetime
|
||||||
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
|
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
|
||||||
|
@ -600,6 +601,7 @@ class AwesomeStrategy(IStrategy):
|
||||||
:param order_type: Order type (as configured in order_types). usually limit or market.
|
:param order_type: Order type (as configured in order_types). usually limit or market.
|
||||||
:param amount: Amount in base currency.
|
:param amount: Amount in base currency.
|
||||||
:param rate: Rate that's going to be used when using limit orders
|
: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 time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
|
||||||
:param exit_reason: Exit reason.
|
:param exit_reason: Exit reason.
|
||||||
Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss',
|
Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss',
|
||||||
|
|
|
@ -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:**
|
> **Daily Profit over the last 3 days:**
|
||||||
```
|
```
|
||||||
Day Profit BTC Profit USD
|
Day (count) USDT USD Profit %
|
||||||
---------- -------------- ------------
|
-------------- ------------ ---------- ----------
|
||||||
2018-01-03 0.00224175 BTC 29,142 USD
|
2022-06-11 (1) -0.746 USDT -0.75 USD -0.08%
|
||||||
2018-01-02 0.00033131 BTC 4,307 USD
|
2022-06-10 (0) 0 USDT 0.00 USD 0.00%
|
||||||
2018-01-01 0.00269130 BTC 34.986 USD
|
2022-06-09 (5) 20 USDT 20.10 USD 5.00%
|
||||||
```
|
```
|
||||||
|
|
||||||
### /weekly <n>
|
### /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):**
|
> **Weekly Profit over the last 3 weeks (starting from Monday):**
|
||||||
```
|
```
|
||||||
Monday Profit BTC Profit USD
|
Monday (count) Profit BTC Profit USD Profit %
|
||||||
---------- -------------- ------------
|
------------- -------------- ------------ ----------
|
||||||
2018-01-03 0.00224175 BTC 29,142 USD
|
2018-01-03 (5) 0.00224175 BTC 29,142 USD 4.98%
|
||||||
2017-12-27 0.00033131 BTC 4,307 USD
|
2017-12-27 (1) 0.00033131 BTC 4,307 USD 0.00%
|
||||||
2017-12-20 0.00269130 BTC 34.986 USD
|
2017-12-20 (4) 0.00269130 BTC 34.986 USD 5.12%
|
||||||
```
|
```
|
||||||
|
|
||||||
### /monthly <n>
|
### /monthly <n>
|
||||||
|
@ -356,11 +356,11 @@ if for `/monthly 3`:
|
||||||
|
|
||||||
> **Monthly Profit over the last 3 months:**
|
> **Monthly Profit over the last 3 months:**
|
||||||
```
|
```
|
||||||
Month Profit BTC Profit USD
|
Month (count) Profit BTC Profit USD Profit %
|
||||||
---------- -------------- ------------
|
------------- -------------- ------------ ----------
|
||||||
2018-01 0.00224175 BTC 29,142 USD
|
2018-01 (20) 0.00224175 BTC 29,142 USD 4.98%
|
||||||
2017-12 0.00033131 BTC 4,307 USD
|
2017-12 (5) 0.00033131 BTC 4,307 USD 0.00%
|
||||||
2017-11 0.00269130 BTC 34.986 USD
|
2017-11 (10) 0.00269130 BTC 34.986 USD 5.10%
|
||||||
```
|
```
|
||||||
|
|
||||||
### /whitelist
|
### /whitelist
|
||||||
|
|
|
@ -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 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}`.
|
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.
|
||||||
|
|
||||||
|
![discord-notification](assets/discord_notification.png)
|
||||||
|
|
|
@ -336,6 +336,47 @@ CONF_SCHEMA = {
|
||||||
'webhookstatus': {'type': 'object'},
|
'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': {
|
'api_server': {
|
||||||
'type': 'object',
|
'type': 'object',
|
||||||
'properties': {
|
'properties': {
|
||||||
|
|
|
@ -429,7 +429,7 @@ class Hyperopt:
|
||||||
return new_list
|
return new_list
|
||||||
i = 0
|
i = 0
|
||||||
asked_non_tried: List[List[Any]] = []
|
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:
|
while i < 5 and len(asked_non_tried) < n_points:
|
||||||
if i < 3:
|
if i < 3:
|
||||||
self.opt.cache_ = {}
|
self.opt.cache_ = {}
|
||||||
|
@ -438,7 +438,7 @@ class Hyperopt:
|
||||||
else:
|
else:
|
||||||
asked = unique_list(self.opt.space.rvs(n_samples=n_points * 5))
|
asked = unique_list(self.opt.space.rvs(n_samples=n_points * 5))
|
||||||
is_random = [True for _ in range(len(asked))]
|
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
|
if x not in self.opt.Xi
|
||||||
and x not in asked_non_tried]
|
and x not in asked_non_tried]
|
||||||
asked_non_tried += [x for x in asked
|
asked_non_tried += [x for x in asked
|
||||||
|
@ -449,7 +449,7 @@ class Hyperopt:
|
||||||
if asked_non_tried:
|
if asked_non_tried:
|
||||||
return (
|
return (
|
||||||
asked_non_tried[:min(len(asked_non_tried), n_points)],
|
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:
|
else:
|
||||||
return self.opt.ask(n_points=n_points), [False for _ in range(n_points)]
|
return self.opt.ask(n_points=n_points), [False for _ in range(n_points)]
|
||||||
|
|
|
@ -76,7 +76,7 @@ class Order(_DECL_BASE):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def safe_filled(self) -> float:
|
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
|
@property
|
||||||
def safe_fee_base(self) -> float:
|
def safe_fee_base(self) -> float:
|
||||||
|
@ -850,8 +850,6 @@ class LocalTrade():
|
||||||
|
|
||||||
tmp_amount = o.safe_amount_after_fee
|
tmp_amount = o.safe_amount_after_fee
|
||||||
tmp_price = o.average or o.price
|
tmp_price = o.average or o.price
|
||||||
if o.filled is not None:
|
|
||||||
tmp_amount = o.filled
|
|
||||||
if tmp_amount > 0.0 and tmp_price is not None:
|
if tmp_amount > 0.0 and tmp_price is not None:
|
||||||
total_amount += tmp_amount
|
total_amount += tmp_amount
|
||||||
total_stake += tmp_price * tmp_amount
|
total_stake += tmp_price * tmp_amount
|
||||||
|
|
|
@ -120,6 +120,8 @@ class Stats(BaseModel):
|
||||||
class DailyRecord(BaseModel):
|
class DailyRecord(BaseModel):
|
||||||
date: date
|
date: date
|
||||||
abs_profit: float
|
abs_profit: float
|
||||||
|
rel_profit: float
|
||||||
|
starting_balance: float
|
||||||
fiat_value: float
|
fiat_value: float
|
||||||
trade_count: int
|
trade_count: int
|
||||||
|
|
||||||
|
|
|
@ -36,7 +36,8 @@ logger = logging.getLogger(__name__)
|
||||||
# versions 2.xx -> futures/short branch
|
# versions 2.xx -> futures/short branch
|
||||||
# 2.14: Add entry/exit orders to trade response
|
# 2.14: Add entry/exit orders to trade response
|
||||||
# 2.15: Add backtest history endpoints
|
# 2.15: Add backtest history endpoints
|
||||||
API_VERSION = 2.15
|
# 2.16: Additional daily metrics
|
||||||
|
API_VERSION = 2.16
|
||||||
|
|
||||||
# Public API, requires no auth.
|
# Public API, requires no auth.
|
||||||
router_public = APIRouter()
|
router_public = APIRouter()
|
||||||
|
@ -86,7 +87,7 @@ def stats(rpc: RPC = Depends(get_rpc)):
|
||||||
|
|
||||||
@router.get('/daily', response_model=Daily, tags=['info'])
|
@router.get('/daily', response_model=Daily, tags=['info'])
|
||||||
def daily(timescale: int = 7, rpc: RPC = Depends(get_rpc), config=Depends(get_config)):
|
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', ''))
|
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')
|
columns.append('# Entries')
|
||||||
return trades_list, columns, fiat_profit_sum
|
return trades_list, columns, fiat_profit_sum
|
||||||
|
|
||||||
def _rpc_daily_profit(
|
def _rpc_timeunit_profit(
|
||||||
self, timescale: int,
|
self, timescale: int,
|
||||||
stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]:
|
stake_currency: str, fiat_display_currency: str,
|
||||||
today = datetime.now(timezone.utc).date()
|
timeunit: str = 'days') -> Dict[str, Any]:
|
||||||
profit_days: Dict[date, Dict] = {}
|
"""
|
||||||
|
: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):
|
if not (isinstance(timescale, int) and timescale > 0):
|
||||||
raise RPCException('timescale must be an integer greater than 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):
|
for day in range(0, timescale):
|
||||||
profitday = today - timedelta(days=day)
|
profitday = start_date - time_offset(day)
|
||||||
trades = Trade.get_trades(trade_filter=[
|
# Only query for necessary columns for performance reasons.
|
||||||
|
trades = Trade.query.session.query(Trade.close_profit_abs).filter(
|
||||||
Trade.is_open.is_(False),
|
Trade.is_open.is_(False),
|
||||||
Trade.close_date >= profitday,
|
Trade.close_date >= profitday,
|
||||||
Trade.close_date < (profitday + timedelta(days=1))
|
Trade.close_date < (profitday + time_offset(1))
|
||||||
]).order_by(Trade.close_date).all()
|
).order_by(Trade.close_date).all()
|
||||||
|
|
||||||
curdayprofit = sum(
|
curdayprofit = sum(
|
||||||
trade.close_profit_abs for trade in trades if trade.close_profit_abs is not None)
|
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,
|
'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 = [
|
data = [
|
||||||
{
|
{
|
||||||
'date': key,
|
'date': f"{key.year}-{key.month:02d}" if timeunit == 'months' else key,
|
||||||
'abs_profit': value["amount"],
|
'abs_profit': value["amount"],
|
||||||
|
'starting_balance': value["daily_stake"],
|
||||||
|
'rel_profit': value["rel_profit"],
|
||||||
'fiat_value': self._fiat_converter.convert_amount(
|
'fiat_value': self._fiat_converter.convert_amount(
|
||||||
value['amount'],
|
value['amount'],
|
||||||
stake_currency,
|
stake_currency,
|
||||||
|
@ -317,92 +341,7 @@ class RPC:
|
||||||
) if self._fiat_converter else 0,
|
) if self._fiat_converter else 0,
|
||||||
'trade_count': value["trades"],
|
'trade_count': value["trades"],
|
||||||
}
|
}
|
||||||
for key, value in profit_days.items()
|
for key, value in profit_units.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()
|
|
||||||
]
|
]
|
||||||
return {
|
return {
|
||||||
'stake_currency': stake_currency,
|
'stake_currency': stake_currency,
|
||||||
|
|
|
@ -27,6 +27,12 @@ class RPCManager:
|
||||||
from freqtrade.rpc.telegram import Telegram
|
from freqtrade.rpc.telegram import Telegram
|
||||||
self.registered_modules.append(Telegram(self._rpc, config))
|
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
|
# Enable Webhook
|
||||||
if config.get('webhook', {}).get('enabled', False):
|
if config.get('webhook', {}).get('enabled', False):
|
||||||
logger.info('Enabling rpc.webhook ...')
|
logger.info('Enabling rpc.webhook ...')
|
||||||
|
|
|
@ -6,6 +6,7 @@ This module manage Telegram communication
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
from dataclasses import dataclass
|
||||||
from datetime import date, datetime, timedelta
|
from datetime import date, datetime, timedelta
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from html import escape
|
from html import escape
|
||||||
|
@ -37,6 +38,15 @@ logger.debug('Included module rpc.telegram ...')
|
||||||
MAX_TELEGRAM_MESSAGE_LENGTH = 4096
|
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]:
|
def authorized_only(command_handler: Callable[..., None]) -> Callable[..., Any]:
|
||||||
"""
|
"""
|
||||||
Decorator to check if the message comes from the correct chat_id
|
Decorator to check if the message comes from the correct chat_id
|
||||||
|
@ -564,6 +574,60 @@ class Telegram(RPCHandler):
|
||||||
except RPCException as e:
|
except RPCException as e:
|
||||||
self._send_msg(str(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
|
@authorized_only
|
||||||
def _daily(self, update: Update, context: CallbackContext) -> None:
|
def _daily(self, update: Update, context: CallbackContext) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -573,35 +637,7 @@ class Telegram(RPCHandler):
|
||||||
:param update: message update
|
:param update: message update
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
stake_cur = self._config['stake_currency']
|
self._timeunit_stats(update, context, 'days')
|
||||||
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))
|
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _weekly(self, update: Update, context: CallbackContext) -> None:
|
def _weekly(self, update: Update, context: CallbackContext) -> None:
|
||||||
|
@ -612,36 +648,7 @@ class Telegram(RPCHandler):
|
||||||
:param update: message update
|
:param update: message update
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
stake_cur = self._config['stake_currency']
|
self._timeunit_stats(update, context, 'weeks')
|
||||||
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))
|
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _monthly(self, update: Update, context: CallbackContext) -> None:
|
def _monthly(self, update: Update, context: CallbackContext) -> None:
|
||||||
|
@ -652,36 +659,7 @@ class Telegram(RPCHandler):
|
||||||
:param update: message update
|
:param update: message update
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
stake_cur = self._config['stake_currency']
|
self._timeunit_stats(update, context, 'months')
|
||||||
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))
|
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _profit(self, update: Update, context: CallbackContext) -> None:
|
def _profit(self, update: Update, context: CallbackContext) -> None:
|
||||||
|
|
|
@ -289,6 +289,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||||
:param order_type: Order type (as configured in order_types). usually limit or market.
|
:param order_type: Order type (as configured in order_types). usually limit or market.
|
||||||
:param amount: Amount in target (base) 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
|
: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 time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
|
||||||
:param current_time: datetime object, containing the current datetime
|
:param current_time: datetime object, containing the current datetime
|
||||||
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
|
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
|
||||||
|
@ -316,6 +317,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||||
:param order_type: Order type (as configured in order_types). usually limit or market.
|
:param order_type: Order type (as configured in order_types). usually limit or market.
|
||||||
:param amount: Amount in base currency.
|
:param amount: Amount in base currency.
|
||||||
:param rate: Rate that's going to be used when using limit orders
|
: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 time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
|
||||||
:param exit_reason: Exit reason.
|
:param exit_reason: Exit reason.
|
||||||
Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss',
|
Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss',
|
||||||
|
|
|
@ -161,6 +161,7 @@ def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: f
|
||||||
:param order_type: Order type (as configured in order_types). usually limit or market.
|
:param order_type: Order type (as configured in order_types). usually limit or market.
|
||||||
:param amount: Amount in target (base) 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
|
: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 time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
|
||||||
:param current_time: datetime object, containing the current datetime
|
:param current_time: datetime object, containing the current datetime
|
||||||
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
|
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
|
||||||
|
@ -188,6 +189,7 @@ def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount:
|
||||||
:param order_type: Order type (as configured in order_types). usually limit or market.
|
:param order_type: Order type (as configured in order_types). usually limit or market.
|
||||||
:param amount: Amount in base currency.
|
:param amount: Amount in base currency.
|
||||||
:param rate: Rate that's going to be used when using limit orders
|
: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 time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
|
||||||
:param exit_reason: Exit reason.
|
:param exit_reason: Exit reason.
|
||||||
Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss',
|
Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss',
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
coveralls==3.3.1
|
coveralls==3.3.1
|
||||||
flake8==4.0.1
|
flake8==4.0.1
|
||||||
flake8-tidy-imports==4.8.0
|
flake8-tidy-imports==4.8.0
|
||||||
mypy==0.960
|
mypy==0.961
|
||||||
pre-commit==2.19.0
|
pre-commit==2.19.0
|
||||||
pytest==7.1.2
|
pytest==7.1.2
|
||||||
pytest-asyncio==0.18.3
|
pytest-asyncio==0.18.3
|
||||||
|
@ -23,7 +23,7 @@ nbconvert==6.5.0
|
||||||
|
|
||||||
# mypy types
|
# mypy types
|
||||||
types-cachetools==5.0.1
|
types-cachetools==5.0.1
|
||||||
types-filelock==3.2.6
|
types-filelock==3.2.7
|
||||||
types-requests==2.27.30
|
types-requests==2.27.30
|
||||||
types-tabulate==0.8.9
|
types-tabulate==0.8.9
|
||||||
types-python-dateutil==2.8.17
|
types-python-dateutil==2.8.17
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Include all requirements to run the bot.
|
# Include all requirements to run the bot.
|
||||||
-r requirements.txt
|
-r requirements.txt
|
||||||
|
|
||||||
plotly==5.8.0
|
plotly==5.8.2
|
||||||
|
|
|
@ -2,7 +2,7 @@ numpy==1.22.4
|
||||||
pandas==1.4.2
|
pandas==1.4.2
|
||||||
pandas-ta==0.3.14b
|
pandas-ta==0.3.14b
|
||||||
|
|
||||||
ccxt==1.85.57
|
ccxt==1.87.12
|
||||||
# Pin cryptography for now due to rust build errors with piwheels
|
# Pin cryptography for now due to rust build errors with piwheels
|
||||||
cryptography==37.0.2
|
cryptography==37.0.2
|
||||||
aiohttp==3.8.1
|
aiohttp==3.8.1
|
||||||
|
@ -10,7 +10,7 @@ SQLAlchemy==1.4.37
|
||||||
python-telegram-bot==13.12
|
python-telegram-bot==13.12
|
||||||
arrow==1.2.2
|
arrow==1.2.2
|
||||||
cachetools==4.2.2
|
cachetools==4.2.2
|
||||||
requests==2.27.1
|
requests==2.28.0
|
||||||
urllib3==1.26.9
|
urllib3==1.26.9
|
||||||
jsonschema==4.6.0
|
jsonschema==4.6.0
|
||||||
TA-Lib==0.4.24
|
TA-Lib==0.4.24
|
||||||
|
@ -28,7 +28,7 @@ py_find_1st==1.1.5
|
||||||
# Load ticker files 30% faster
|
# Load ticker files 30% faster
|
||||||
python-rapidjson==1.6
|
python-rapidjson==1.6
|
||||||
# Properly format api responses
|
# Properly format api responses
|
||||||
orjson==3.7.1
|
orjson==3.7.2
|
||||||
|
|
||||||
# Notify systemd
|
# Notify systemd
|
||||||
sdnotify==0.3.2
|
sdnotify==0.3.2
|
||||||
|
|
4
setup.sh
4
setup.sh
|
@ -87,6 +87,10 @@ function updateenv() {
|
||||||
echo "Failed installing Freqtrade"
|
echo "Failed installing Freqtrade"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echo "Installing freqUI"
|
||||||
|
freqtrade install-ui
|
||||||
|
|
||||||
echo "pip install completed"
|
echo "pip install completed"
|
||||||
echo
|
echo
|
||||||
if [[ $dev =~ ^[Yy]$ ]]; then
|
if [[ $dev =~ ^[Yy]$ ]]; then
|
||||||
|
|
|
@ -325,7 +325,7 @@ def create_mock_trades_with_leverage(fee, use_db: bool = True):
|
||||||
Trade.query.session.flush()
|
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 ...
|
Create some fake trades ...
|
||||||
"""
|
"""
|
||||||
|
@ -335,26 +335,29 @@ def create_mock_trades_usdt(fee, use_db: bool = True):
|
||||||
else:
|
else:
|
||||||
LocalTrade.add_bt_trade(trade)
|
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
|
# Simulate dry_run entries
|
||||||
trade = mock_trade_usdt_1(fee)
|
trade = mock_trade_usdt_1(fee, is_short1)
|
||||||
add_trade(trade)
|
add_trade(trade)
|
||||||
|
|
||||||
trade = mock_trade_usdt_2(fee)
|
trade = mock_trade_usdt_2(fee, is_short1)
|
||||||
add_trade(trade)
|
add_trade(trade)
|
||||||
|
|
||||||
trade = mock_trade_usdt_3(fee)
|
trade = mock_trade_usdt_3(fee, is_short1)
|
||||||
add_trade(trade)
|
add_trade(trade)
|
||||||
|
|
||||||
trade = mock_trade_usdt_4(fee)
|
trade = mock_trade_usdt_4(fee, is_short2)
|
||||||
add_trade(trade)
|
add_trade(trade)
|
||||||
|
|
||||||
trade = mock_trade_usdt_5(fee)
|
trade = mock_trade_usdt_5(fee, is_short2)
|
||||||
add_trade(trade)
|
add_trade(trade)
|
||||||
|
|
||||||
trade = mock_trade_usdt_6(fee)
|
trade = mock_trade_usdt_6(fee, is_short1)
|
||||||
add_trade(trade)
|
add_trade(trade)
|
||||||
|
|
||||||
trade = mock_trade_usdt_7(fee)
|
trade = mock_trade_usdt_7(fee, is_short1)
|
||||||
add_trade(trade)
|
add_trade(trade)
|
||||||
if use_db:
|
if use_db:
|
||||||
Trade.commit()
|
Trade.commit()
|
||||||
|
|
|
@ -6,47 +6,84 @@ from freqtrade.persistence.models import Order, Trade
|
||||||
MOCK_TRADE_COUNT = 6
|
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 {
|
return {
|
||||||
'id': '1234',
|
'id': f'prod_entry_1_{direc(is_short)}',
|
||||||
'symbol': 'ADA/USDT',
|
'symbol': 'LTC/USDT',
|
||||||
'status': 'closed',
|
'status': 'closed',
|
||||||
'side': 'buy',
|
'side': entry_side(is_short),
|
||||||
'type': 'limit',
|
'type': 'limit',
|
||||||
'price': 2.0,
|
'price': 10.0,
|
||||||
'amount': 10.0,
|
'amount': 2.0,
|
||||||
'filled': 10.0,
|
'filled': 2.0,
|
||||||
'remaining': 0.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(
|
trade = Trade(
|
||||||
pair='ADA/USDT',
|
pair='LTC/USDT',
|
||||||
stake_amount=20.0,
|
stake_amount=20.0,
|
||||||
amount=10.0,
|
amount=2.0,
|
||||||
amount_requested=10.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_open=fee.return_value,
|
||||||
fee_close=fee.return_value,
|
fee_close=fee.return_value,
|
||||||
is_open=True,
|
is_open=False,
|
||||||
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=17),
|
open_rate=10.0,
|
||||||
open_rate=2.0,
|
close_rate=8.0,
|
||||||
|
close_profit=-0.2,
|
||||||
|
close_profit_abs=-4.0,
|
||||||
exchange='binance',
|
exchange='binance',
|
||||||
open_order_id='dry_run_buy_12345',
|
strategy='SampleStrategy',
|
||||||
strategy='StrategyTestV2',
|
open_order_id=f'prod_exit_1_{direc(is_short)}',
|
||||||
timeframe=5,
|
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)
|
trade.orders.append(o)
|
||||||
return trade
|
return trade
|
||||||
|
|
||||||
|
|
||||||
def mock_order_usdt_2():
|
def mock_order_usdt_2(is_short: bool):
|
||||||
return {
|
return {
|
||||||
'id': '1235',
|
'id': f'1235_{direc(is_short)}',
|
||||||
'symbol': 'ETC/USDT',
|
'symbol': 'ETC/USDT',
|
||||||
'status': 'closed',
|
'status': 'closed',
|
||||||
'side': 'buy',
|
'side': entry_side(is_short),
|
||||||
'type': 'limit',
|
'type': 'limit',
|
||||||
'price': 2.0,
|
'price': 2.0,
|
||||||
'amount': 100.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 {
|
return {
|
||||||
'id': '12366',
|
'id': f'12366_{direc(is_short)}',
|
||||||
'symbol': 'ETC/USDT',
|
'symbol': 'ETC/USDT',
|
||||||
'status': 'closed',
|
'status': 'closed',
|
||||||
'side': 'sell',
|
'side': exit_side(is_short),
|
||||||
'type': 'limit',
|
'type': 'limit',
|
||||||
'price': 2.05,
|
'price': 2.05,
|
||||||
'amount': 100.0,
|
'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...
|
Closed trade...
|
||||||
"""
|
"""
|
||||||
|
@ -82,30 +119,33 @@ def mock_trade_usdt_2(fee):
|
||||||
fee_close=fee.return_value,
|
fee_close=fee.return_value,
|
||||||
open_rate=2.0,
|
open_rate=2.0,
|
||||||
close_rate=2.05,
|
close_rate=2.05,
|
||||||
close_profit=5.0,
|
close_profit=0.05,
|
||||||
close_profit_abs=3.9875,
|
close_profit_abs=3.9875,
|
||||||
exchange='binance',
|
exchange='binance',
|
||||||
is_open=False,
|
is_open=False,
|
||||||
open_order_id='dry_run_sell_12345',
|
open_order_id=f'12366_{direc(is_short)}',
|
||||||
strategy='StrategyTestV2',
|
strategy='StrategyTestV2',
|
||||||
timeframe=5,
|
timeframe=5,
|
||||||
exit_reason='sell_signal',
|
enter_tag='TEST1',
|
||||||
|
exit_reason='exit_signal',
|
||||||
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20),
|
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20),
|
||||||
close_date=datetime.now(tz=timezone.utc) - timedelta(minutes=2),
|
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)
|
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)
|
trade.orders.append(o)
|
||||||
return trade
|
return trade
|
||||||
|
|
||||||
|
|
||||||
def mock_order_usdt_3():
|
def mock_order_usdt_3(is_short: bool):
|
||||||
return {
|
return {
|
||||||
'id': '41231a12a',
|
'id': f'41231a12a_{direc(is_short)}',
|
||||||
'symbol': 'XRP/USDT',
|
'symbol': 'XRP/USDT',
|
||||||
'status': 'closed',
|
'status': 'closed',
|
||||||
'side': 'buy',
|
'side': entry_side(is_short),
|
||||||
'type': 'limit',
|
'type': 'limit',
|
||||||
'price': 1.0,
|
'price': 1.0,
|
||||||
'amount': 30.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 {
|
return {
|
||||||
'id': '41231a666a',
|
'id': f'41231a666a_{direc(is_short)}',
|
||||||
'symbol': 'XRP/USDT',
|
'symbol': 'XRP/USDT',
|
||||||
'status': 'closed',
|
'status': 'closed',
|
||||||
'side': 'sell',
|
'side': exit_side(is_short),
|
||||||
'type': 'stop_loss_limit',
|
'type': 'stop_loss_limit',
|
||||||
'price': 1.1,
|
'price': 1.1,
|
||||||
'average': 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
|
Closed trade
|
||||||
"""
|
"""
|
||||||
|
@ -142,29 +182,32 @@ def mock_trade_usdt_3(fee):
|
||||||
fee_close=fee.return_value,
|
fee_close=fee.return_value,
|
||||||
open_rate=1.0,
|
open_rate=1.0,
|
||||||
close_rate=1.1,
|
close_rate=1.1,
|
||||||
close_profit=10.0,
|
close_profit=0.1,
|
||||||
close_profit_abs=9.8425,
|
close_profit_abs=9.8425,
|
||||||
exchange='binance',
|
exchange='binance',
|
||||||
is_open=False,
|
is_open=False,
|
||||||
strategy='StrategyTestV2',
|
strategy='StrategyTestV2',
|
||||||
timeframe=5,
|
timeframe=5,
|
||||||
|
enter_tag='TEST3',
|
||||||
exit_reason='roi',
|
exit_reason='roi',
|
||||||
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20),
|
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20),
|
||||||
close_date=datetime.now(tz=timezone.utc),
|
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)
|
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)
|
trade.orders.append(o)
|
||||||
return trade
|
return trade
|
||||||
|
|
||||||
|
|
||||||
def mock_order_usdt_4():
|
def mock_order_usdt_4(is_short: bool):
|
||||||
return {
|
return {
|
||||||
'id': 'prod_buy_12345',
|
'id': f'prod_buy_12345_{direc(is_short)}',
|
||||||
'symbol': 'ETC/USDT',
|
'symbol': 'ETC/USDT',
|
||||||
'status': 'open',
|
'status': 'open',
|
||||||
'side': 'buy',
|
'side': entry_side(is_short),
|
||||||
'type': 'limit',
|
'type': 'limit',
|
||||||
'price': 2.0,
|
'price': 2.0,
|
||||||
'amount': 10.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
|
Simulate prod entry
|
||||||
"""
|
"""
|
||||||
|
@ -188,21 +231,22 @@ def mock_trade_usdt_4(fee):
|
||||||
is_open=True,
|
is_open=True,
|
||||||
open_rate=2.0,
|
open_rate=2.0,
|
||||||
exchange='binance',
|
exchange='binance',
|
||||||
open_order_id='prod_buy_12345',
|
open_order_id=f'prod_buy_12345_{direc(is_short)}',
|
||||||
strategy='StrategyTestV2',
|
strategy='StrategyTestV2',
|
||||||
timeframe=5,
|
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)
|
trade.orders.append(o)
|
||||||
return trade
|
return trade
|
||||||
|
|
||||||
|
|
||||||
def mock_order_usdt_5():
|
def mock_order_usdt_5(is_short: bool):
|
||||||
return {
|
return {
|
||||||
'id': 'prod_buy_3455',
|
'id': f'prod_buy_3455_{direc(is_short)}',
|
||||||
'symbol': 'XRP/USDT',
|
'symbol': 'XRP/USDT',
|
||||||
'status': 'closed',
|
'status': 'closed',
|
||||||
'side': 'buy',
|
'side': entry_side(is_short),
|
||||||
'type': 'limit',
|
'type': 'limit',
|
||||||
'price': 2.0,
|
'price': 2.0,
|
||||||
'amount': 10.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 {
|
return {
|
||||||
'id': 'prod_stoploss_3455',
|
'id': f'prod_stoploss_3455_{direc(is_short)}',
|
||||||
'symbol': 'XRP/USDT',
|
'symbol': 'XRP/USDT',
|
||||||
'status': 'open',
|
'status': 'open',
|
||||||
'side': 'sell',
|
'side': exit_side(is_short),
|
||||||
'type': 'stop_loss_limit',
|
'type': 'stop_loss_limit',
|
||||||
'price': 2.0,
|
'price': 2.0,
|
||||||
'amount': 10.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
|
Simulate prod entry with stoploss
|
||||||
"""
|
"""
|
||||||
|
@ -241,22 +285,23 @@ def mock_trade_usdt_5(fee):
|
||||||
open_rate=2.0,
|
open_rate=2.0,
|
||||||
exchange='binance',
|
exchange='binance',
|
||||||
strategy='SampleStrategy',
|
strategy='SampleStrategy',
|
||||||
stoploss_order_id='prod_stoploss_3455',
|
stoploss_order_id=f'prod_stoploss_3455_{direc(is_short)}',
|
||||||
timeframe=5,
|
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)
|
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)
|
trade.orders.append(o)
|
||||||
return trade
|
return trade
|
||||||
|
|
||||||
|
|
||||||
def mock_order_usdt_6():
|
def mock_order_usdt_6(is_short: bool):
|
||||||
return {
|
return {
|
||||||
'id': 'prod_buy_6',
|
'id': f'prod_entry_6_{direc(is_short)}',
|
||||||
'symbol': 'LTC/USDT',
|
'symbol': 'LTC/USDT',
|
||||||
'status': 'closed',
|
'status': 'closed',
|
||||||
'side': 'buy',
|
'side': entry_side(is_short),
|
||||||
'type': 'limit',
|
'type': 'limit',
|
||||||
'price': 10.0,
|
'price': 10.0,
|
||||||
'amount': 2.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 {
|
return {
|
||||||
'id': 'prod_sell_6',
|
'id': f'prod_exit_6_{direc(is_short)}',
|
||||||
'symbol': 'LTC/USDT',
|
'symbol': 'LTC/USDT',
|
||||||
'status': 'open',
|
'status': 'open',
|
||||||
'side': 'sell',
|
'side': exit_side(is_short),
|
||||||
'type': 'limit',
|
'type': 'limit',
|
||||||
'price': 12.0,
|
'price': 12.0,
|
||||||
'amount': 2.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
|
Simulate prod entry with open sell order
|
||||||
"""
|
"""
|
||||||
|
@ -295,69 +340,49 @@ def mock_trade_usdt_6(fee):
|
||||||
open_rate=10.0,
|
open_rate=10.0,
|
||||||
exchange='binance',
|
exchange='binance',
|
||||||
strategy='SampleStrategy',
|
strategy='SampleStrategy',
|
||||||
open_order_id="prod_sell_6",
|
open_order_id=f'prod_exit_6_{direc(is_short)}',
|
||||||
timeframe=5,
|
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)
|
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)
|
trade.orders.append(o)
|
||||||
return trade
|
return trade
|
||||||
|
|
||||||
|
|
||||||
def mock_order_usdt_7():
|
def mock_order_usdt_7(is_short: bool):
|
||||||
return {
|
return {
|
||||||
'id': 'prod_buy_7',
|
'id': f'1234_{direc(is_short)}',
|
||||||
'symbol': 'LTC/USDT',
|
'symbol': 'ADA/USDT',
|
||||||
'status': 'closed',
|
'status': 'closed',
|
||||||
'side': 'buy',
|
'side': entry_side(is_short),
|
||||||
'type': 'limit',
|
'type': 'limit',
|
||||||
'price': 10.0,
|
'price': 2.0,
|
||||||
'amount': 2.0,
|
'amount': 10.0,
|
||||||
'filled': 2.0,
|
'filled': 10.0,
|
||||||
'remaining': 0.0,
|
'remaining': 0.0,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def mock_order_usdt_7_sell():
|
def mock_trade_usdt_7(fee, is_short: bool):
|
||||||
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
|
|
||||||
"""
|
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
pair='LTC/USDT',
|
pair='ADA/USDT',
|
||||||
stake_amount=20.0,
|
stake_amount=20.0,
|
||||||
amount=2.0,
|
amount=10.0,
|
||||||
amount_requested=2.0,
|
amount_requested=10.0,
|
||||||
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20),
|
|
||||||
close_date=datetime.now(tz=timezone.utc) - timedelta(minutes=5),
|
|
||||||
fee_open=fee.return_value,
|
fee_open=fee.return_value,
|
||||||
fee_close=fee.return_value,
|
fee_close=fee.return_value,
|
||||||
is_open=False,
|
is_open=True,
|
||||||
open_rate=10.0,
|
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=17),
|
||||||
close_rate=8.0,
|
open_rate=2.0,
|
||||||
close_profit=-0.2,
|
|
||||||
close_profit_abs=-4.0,
|
|
||||||
exchange='binance',
|
exchange='binance',
|
||||||
strategy='SampleStrategy',
|
open_order_id=f'1234_{direc(is_short)}',
|
||||||
open_order_id="prod_sell_6",
|
strategy='StrategyTestV2',
|
||||||
timeframe=5,
|
timeframe=5,
|
||||||
|
is_short=is_short,
|
||||||
)
|
)
|
||||||
o = Order.parse_from_ccxt_object(mock_order_usdt_7(), 'LTC/USDT', 'buy')
|
o = Order.parse_from_ccxt_object(mock_order_usdt_7(is_short), 'ADA/USDT', entry_side(is_short))
|
||||||
trade.orders.append(o)
|
|
||||||
o = Order.parse_from_ccxt_object(mock_order_usdt_7_sell(), 'LTC/USDT', 'sell')
|
|
||||||
trade.orders.append(o)
|
trade.orders.append(o)
|
||||||
return trade
|
return trade
|
||||||
|
|
|
@ -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:
|
with time_machine.travel("2021-09-01 05:00:00 +00:00") as t:
|
||||||
create_mock_trades_usdt(fee)
|
create_mock_trades_usdt(fee)
|
||||||
pm.refresh_pairlist()
|
pm.refresh_pairlist()
|
||||||
assert pm.whitelist == ['XRP/USDT', 'ETC/USDT', 'ETH/USDT',
|
assert pm.whitelist == ['XRP/USDT', 'ETC/USDT', 'ETH/USDT', 'LTC/USDT',
|
||||||
'NEO/USDT', 'TKN/USDT', 'ADA/USDT', 'LTC/USDT']
|
'NEO/USDT', 'TKN/USDT', 'ADA/USDT', ]
|
||||||
# assert log_has_re(r'Removing pair .* since .* is below .*', caplog)
|
# assert log_has_re(r'Removing pair .* since .* is below .*', caplog)
|
||||||
|
|
||||||
# Move to "outside" of lookback window, so original sorting is restored.
|
# 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.enums import SignalDirection, State, TradingMode
|
||||||
from freqtrade.exceptions import ExchangeError, InvalidOrderException, TemporaryError
|
from freqtrade.exceptions import ExchangeError, InvalidOrderException, TemporaryError
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
from freqtrade.persistence.models import Order
|
|
||||||
from freqtrade.persistence.pairlock_middleware import PairLocks
|
from freqtrade.persistence.pairlock_middleware import PairLocks
|
||||||
from freqtrade.rpc import RPC, RPCException
|
from freqtrade.rpc import RPC, RPCException
|
||||||
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
|
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
|
# Functions for recurrent object patching
|
||||||
|
@ -284,7 +284,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None:
|
||||||
assert isnan(fiat_profit_sum)
|
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:
|
limit_buy_order, limit_sell_order, markets, mocker) -> None:
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
|
@ -294,45 +294,35 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee,
|
||||||
markets=PropertyMock(return_value=markets)
|
markets=PropertyMock(return_value=markets)
|
||||||
)
|
)
|
||||||
|
|
||||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
freqtradebot = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||||
patch_get_signal(freqtradebot)
|
create_mock_trades_usdt(fee)
|
||||||
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)
|
rpc = RPC(freqtradebot)
|
||||||
rpc._fiat_converter = CryptoToFiatConverter()
|
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
|
# Try valid data
|
||||||
update.message.text = '/daily 2'
|
days = rpc._rpc_timeunit_profit(7, stake_currency, fiat_display_currency)
|
||||||
days = rpc._rpc_daily_profit(7, stake_currency, fiat_display_currency)
|
|
||||||
assert len(days['data']) == 7
|
assert len(days['data']) == 7
|
||||||
assert days['stake_currency'] == default_conf['stake_currency']
|
assert days['stake_currency'] == default_conf_usdt['stake_currency']
|
||||||
assert days['fiat_display_currency'] == default_conf['fiat_display_currency']
|
assert days['fiat_display_currency'] == default_conf_usdt['fiat_display_currency']
|
||||||
for day in days['data']:
|
for day in days['data']:
|
||||||
# [datetime.date(2018, 1, 11), '0.00000000 BTC', '0.000 USD']
|
# {'date': datetime.date(2022, 6, 11), 'abs_profit': 13.8299999,
|
||||||
assert (day['abs_profit'] == 0.0 or
|
# 'starting_balance': 1055.37, 'rel_profit': 0.0131044,
|
||||||
day['abs_profit'] == 0.00006217)
|
# 'fiat_value': 0.0, 'trade_count': 2}
|
||||||
|
assert day['abs_profit'] in (0.0, pytest.approx(13.8299999), pytest.approx(-4.0))
|
||||||
assert (day['fiat_value'] == 0.0 or
|
assert day['rel_profit'] in (0.0, pytest.approx(0.01310441), pytest.approx(-0.00377583))
|
||||||
day['fiat_value'] == 0.76748865)
|
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
|
# ensure first day is current date
|
||||||
assert str(days['data'][0]['date']) == str(datetime.utcnow().date())
|
assert str(days['data'][0]['date']) == str(datetime.utcnow().date())
|
||||||
|
|
||||||
# Try invalid data
|
# Try invalid data
|
||||||
with pytest.raises(RPCException, match=r'.*must be an integer greater than 0*'):
|
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])
|
@pytest.mark.parametrize('is_short', [True, False])
|
||||||
|
@ -416,13 +406,8 @@ def test_rpc_delete_trade(mocker, default_conf, fee, markets, caplog, is_short):
|
||||||
assert stoploss_mock.call_count == 0
|
assert stoploss_mock.call_count == 0
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
|
def test_rpc_trade_statistics(default_conf_usdt, ticker, fee, mocker) -> None:
|
||||||
limit_buy_order, limit_sell_order, mocker) -> None:
|
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=1.1)
|
||||||
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)
|
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
|
@ -430,10 +415,9 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
|
||||||
get_fee=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_usdt['stake_currency']
|
||||||
stake_currency = default_conf['stake_currency']
|
fiat_display_currency = default_conf_usdt['fiat_display_currency']
|
||||||
fiat_display_currency = default_conf['fiat_display_currency']
|
|
||||||
|
|
||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
rpc._fiat_converter = CryptoToFiatConverter()
|
rpc._fiat_converter = CryptoToFiatConverter()
|
||||||
|
@ -446,75 +430,40 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
|
||||||
assert res['latest_trade_timestamp'] == 0
|
assert res['latest_trade_timestamp'] == 0
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
freqtradebot.enter_positions()
|
create_mock_trades_usdt(fee)
|
||||||
trade = Trade.query.first()
|
|
||||||
# Simulate fulfilled LIMIT_BUY order for trade
|
|
||||||
oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'sell')
|
|
||||||
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
|
|
||||||
|
|
||||||
stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
|
stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
|
||||||
assert prec_satoshi(stats['profit_closed_coin'], 6.217e-05)
|
assert pytest.approx(stats['profit_closed_coin']) == 9.83
|
||||||
assert prec_satoshi(stats['profit_closed_percent_mean'], 6.2)
|
assert pytest.approx(stats['profit_closed_percent_mean']) == -1.67
|
||||||
assert prec_satoshi(stats['profit_closed_fiat'], 0.93255)
|
assert pytest.approx(stats['profit_closed_fiat']) == 10.813
|
||||||
assert prec_satoshi(stats['profit_all_coin'], 5.802e-05)
|
assert pytest.approx(stats['profit_all_coin']) == -77.45964918
|
||||||
assert prec_satoshi(stats['profit_all_percent_mean'], 2.89)
|
assert pytest.approx(stats['profit_all_percent_mean']) == -57.86
|
||||||
assert prec_satoshi(stats['profit_all_fiat'], 0.8703)
|
assert pytest.approx(stats['profit_all_fiat']) == -85.205614098
|
||||||
assert stats['trade_count'] == 2
|
assert stats['trade_count'] == 7
|
||||||
assert stats['first_trade_date'] == 'just now'
|
assert stats['first_trade_date'] == '2 days ago'
|
||||||
assert stats['latest_trade_date'] == 'just now'
|
assert stats['latest_trade_date'] == '17 minutes ago'
|
||||||
assert stats['avg_duration'] in ('0:00:00', '0:00:01', '0:00:02')
|
assert stats['avg_duration'] in ('0:17:40')
|
||||||
assert stats['best_pair'] == 'ETH/BTC'
|
assert stats['best_pair'] == 'XRP/USDT'
|
||||||
assert prec_satoshi(stats['best_rate'], 6.2)
|
assert stats['best_rate'] == 10.0
|
||||||
|
|
||||||
# Test non-available pair
|
# Test non-available pair
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_rate',
|
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)
|
stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
|
||||||
assert stats['trade_count'] == 2
|
assert stats['trade_count'] == 7
|
||||||
assert stats['first_trade_date'] == 'just now'
|
assert stats['first_trade_date'] == '2 days ago'
|
||||||
assert stats['latest_trade_date'] == 'just now'
|
assert stats['latest_trade_date'] == '17 minutes ago'
|
||||||
assert stats['avg_duration'] in ('0:00:00', '0:00:01', '0:00:02')
|
assert stats['avg_duration'] in ('0:17:40')
|
||||||
assert stats['best_pair'] == 'ETH/BTC'
|
assert stats['best_pair'] == 'XRP/USDT'
|
||||||
assert prec_satoshi(stats['best_rate'], 6.2)
|
assert stats['best_rate'] == 10.0
|
||||||
assert isnan(stats['profit_all_coin'])
|
assert isnan(stats['profit_all_coin'])
|
||||||
|
|
||||||
|
|
||||||
# Test that rpc_trade_statistics can handle trades that lacks
|
# Test that rpc_trade_statistics can handle trades that lacks
|
||||||
# trade.open_rate (it is set to None)
|
# trade.open_rate (it is set to None)
|
||||||
def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee,
|
def test_rpc_trade_statistics_closed(mocker, default_conf_usdt, 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}}),
|
|
||||||
)
|
|
||||||
mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price',
|
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('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
|
@ -522,46 +471,32 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee,
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
)
|
)
|
||||||
|
|
||||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
freqtradebot = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||||
patch_get_signal(freqtradebot)
|
patch_get_signal(freqtradebot)
|
||||||
stake_currency = default_conf['stake_currency']
|
stake_currency = default_conf_usdt['stake_currency']
|
||||||
fiat_display_currency = default_conf['fiat_display_currency']
|
fiat_display_currency = default_conf_usdt['fiat_display_currency']
|
||||||
|
|
||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
freqtradebot.enter_positions()
|
create_mock_trades_usdt(fee)
|
||||||
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
|
|
||||||
|
|
||||||
for trade in Trade.query.order_by(Trade.id).all():
|
for trade in Trade.query.order_by(Trade.id).all():
|
||||||
trade.open_rate = None
|
trade.open_rate = None
|
||||||
|
|
||||||
stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
|
stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
|
||||||
assert prec_satoshi(stats['profit_closed_coin'], 0)
|
assert stats['profit_closed_coin'] == 0
|
||||||
assert prec_satoshi(stats['profit_closed_percent_mean'], 0)
|
assert stats['profit_closed_percent_mean'] == 0
|
||||||
assert prec_satoshi(stats['profit_closed_fiat'], 0)
|
assert stats['profit_closed_fiat'] == 0
|
||||||
assert prec_satoshi(stats['profit_all_coin'], 0)
|
assert stats['profit_all_coin'] == 0
|
||||||
assert prec_satoshi(stats['profit_all_percent_mean'], 0)
|
assert stats['profit_all_percent_mean'] == 0
|
||||||
assert prec_satoshi(stats['profit_all_fiat'], 0)
|
assert stats['profit_all_fiat'] == 0
|
||||||
assert stats['trade_count'] == 1
|
assert stats['trade_count'] == 7
|
||||||
assert stats['first_trade_date'] == 'just now'
|
assert stats['first_trade_date'] == '2 days ago'
|
||||||
assert stats['latest_trade_date'] == 'just now'
|
assert stats['latest_trade_date'] == '17 minutes ago'
|
||||||
assert stats['avg_duration'] == '0:00:00'
|
assert stats['avg_duration'] == '0:00:00'
|
||||||
assert stats['best_pair'] == 'ETH/BTC'
|
assert stats['best_pair'] == 'XRP/USDT'
|
||||||
assert prec_satoshi(stats['best_rate'], 6.2)
|
assert stats['best_rate'] == 10.0
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_balance_handle_error(default_conf, mocker):
|
def test_rpc_balance_handle_error(default_conf, mocker):
|
||||||
|
@ -913,8 +848,7 @@ def test_rpc_force_exit(default_conf, ticker, fee, mocker) -> None:
|
||||||
assert cancel_order_mock.call_count == 3
|
assert cancel_order_mock.call_count == 3
|
||||||
|
|
||||||
|
|
||||||
def test_performance_handle(default_conf, ticker, limit_buy_order, fee,
|
def test_performance_handle(default_conf_usdt, ticker, fee, mocker) -> None:
|
||||||
limit_sell_order, mocker) -> None:
|
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
|
@ -923,34 +857,21 @@ def test_performance_handle(default_conf, ticker, limit_buy_order, fee,
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
)
|
)
|
||||||
|
|
||||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
freqtradebot = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||||
patch_get_signal(freqtradebot)
|
patch_get_signal(freqtradebot)
|
||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
freqtradebot.enter_positions()
|
create_mock_trades_usdt(fee)
|
||||||
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_performance()
|
res = rpc._rpc_performance()
|
||||||
assert len(res) == 1
|
assert len(res) == 3
|
||||||
assert res[0]['pair'] == 'ETH/BTC'
|
assert res[0]['pair'] == 'XRP/USDT'
|
||||||
assert res[0]['count'] == 1
|
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,
|
def test_enter_tag_performance_handle(default_conf, ticker, fee, mocker) -> None:
|
||||||
limit_sell_order, mocker) -> None:
|
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
|
@ -964,34 +885,22 @@ def test_enter_tag_performance_handle(default_conf, ticker, limit_buy_order, fee
|
||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
|
create_mock_trades_usdt(fee)
|
||||||
freqtradebot.enter_positions()
|
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)
|
res = rpc._rpc_enter_tag_performance(None)
|
||||||
|
|
||||||
assert len(res) == 1
|
assert len(res) == 3
|
||||||
assert res[0]['enter_tag'] == 'Other'
|
assert res[0]['enter_tag'] == 'TEST3'
|
||||||
assert res[0]['count'] == 1
|
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)
|
res = rpc._rpc_enter_tag_performance(None)
|
||||||
|
|
||||||
assert len(res) == 1
|
assert len(res) == 3
|
||||||
assert res[0]['enter_tag'] == 'TEST_TAG'
|
assert res[0]['enter_tag'] == 'TEST3'
|
||||||
assert res[0]['count'] == 1
|
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):
|
def test_enter_tag_performance_handle_2(mocker, default_conf, markets, fee):
|
||||||
|
@ -1023,8 +932,7 @@ def test_enter_tag_performance_handle_2(mocker, default_conf, markets, fee):
|
||||||
assert prec_satoshi(res[0]['profit_pct'], 0.5)
|
assert prec_satoshi(res[0]['profit_pct'], 0.5)
|
||||||
|
|
||||||
|
|
||||||
def test_exit_reason_performance_handle(default_conf, ticker, limit_buy_order, fee,
|
def test_exit_reason_performance_handle(default_conf_usdt, ticker, fee, mocker) -> None:
|
||||||
limit_sell_order, mocker) -> None:
|
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
|
@ -1033,39 +941,22 @@ def test_exit_reason_performance_handle(default_conf, ticker, limit_buy_order, f
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
)
|
)
|
||||||
|
|
||||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
freqtradebot = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||||
patch_get_signal(freqtradebot)
|
patch_get_signal(freqtradebot)
|
||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
freqtradebot.enter_positions()
|
create_mock_trades_usdt(fee)
|
||||||
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_exit_reason_performance(None)
|
res = rpc._rpc_exit_reason_performance(None)
|
||||||
|
|
||||||
assert len(res) == 1
|
assert len(res) == 3
|
||||||
assert res[0]['exit_reason'] == 'Other'
|
assert res[0]['exit_reason'] == 'roi'
|
||||||
assert res[0]['count'] == 1
|
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"
|
assert res[1]['exit_reason'] == 'exit_signal'
|
||||||
res = rpc._rpc_exit_reason_performance(None)
|
assert res[2]['exit_reason'] == 'Other'
|
||||||
|
|
||||||
assert len(res) == 1
|
|
||||||
assert res[0]['exit_reason'] == 'TEST1'
|
|
||||||
assert res[0]['count'] == 1
|
|
||||||
assert prec_satoshi(res[0]['profit_pct'], 6.2)
|
|
||||||
|
|
||||||
|
|
||||||
def test_exit_reason_performance_handle_2(mocker, default_conf, markets, fee):
|
def test_exit_reason_performance_handle_2(mocker, default_conf, markets, fee):
|
||||||
|
@ -1097,8 +988,7 @@ def test_exit_reason_performance_handle_2(mocker, default_conf, markets, fee):
|
||||||
assert prec_satoshi(res[0]['profit_pct'], 0.5)
|
assert prec_satoshi(res[0]['profit_pct'], 0.5)
|
||||||
|
|
||||||
|
|
||||||
def test_mix_tag_performance_handle(default_conf, ticker, limit_buy_order, fee,
|
def test_mix_tag_performance_handle(default_conf, ticker, fee, mocker) -> None:
|
||||||
limit_sell_order, mocker) -> None:
|
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
|
@ -1112,35 +1002,14 @@ def test_mix_tag_performance_handle(default_conf, ticker, limit_buy_order, fee,
|
||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
freqtradebot.enter_positions()
|
create_mock_trades_usdt(fee)
|
||||||
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_mix_tag_performance(None)
|
res = rpc._rpc_mix_tag_performance(None)
|
||||||
|
|
||||||
assert len(res) == 1
|
assert len(res) == 3
|
||||||
assert res[0]['mix_tag'] == 'Other Other'
|
assert res[0]['mix_tag'] == 'TEST3 roi'
|
||||||
assert res[0]['count'] == 1
|
assert res[0]['count'] == 1
|
||||||
assert prec_satoshi(res[0]['profit_pct'], 6.2)
|
assert res[0]['profit_pct'] == 10.0
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
def test_mix_tag_performance_handle_2(mocker, default_conf, markets, fee):
|
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 import RPC
|
||||||
from freqtrade.rpc.rpc import RPCException
|
from freqtrade.rpc.rpc import RPCException
|
||||||
from freqtrade.rpc.telegram import Telegram, authorized_only
|
from freqtrade.rpc.telegram import Telegram, authorized_only
|
||||||
from tests.conftest import (CURRENT_TEST_STRATEGY, create_mock_trades, get_patched_freqtradebot,
|
from tests.conftest import (CURRENT_TEST_STRATEGY, create_mock_trades, create_mock_trades_usdt,
|
||||||
log_has, log_has_re, patch_exchange, patch_get_signal, patch_whitelist)
|
get_patched_freqtradebot, log_has, log_has_re, patch_exchange,
|
||||||
|
patch_get_signal, patch_whitelist)
|
||||||
|
|
||||||
|
|
||||||
class DummyCls(Telegram):
|
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
|
assert msg_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee,
|
def test_daily_handle(default_conf_usdt, update, ticker, fee, mocker, time_machine) -> None:
|
||||||
limit_sell_order, mocker) -> None:
|
|
||||||
default_conf['max_open_trades'] = 1
|
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
'freqtrade.rpc.rpc.CryptoToFiatConverter._find_price',
|
'freqtrade.rpc.rpc.CryptoToFiatConverter._find_price',
|
||||||
return_value=15000.0
|
return_value=1.1
|
||||||
)
|
)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
|
@ -417,25 +416,12 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee,
|
||||||
get_fee=fee,
|
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)
|
|
||||||
|
|
||||||
|
# Move date to within day
|
||||||
|
time_machine.move_to('2022-06-11 08:00:00+00:00')
|
||||||
# Create some test data
|
# Create some test data
|
||||||
freqtradebot.enter_positions()
|
create_mock_trades_usdt(fee)
|
||||||
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
|
|
||||||
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
|
|
||||||
|
|
||||||
# Try valid data
|
# Try valid data
|
||||||
# /daily 2
|
# /daily 2
|
||||||
|
@ -446,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 "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 '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(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 ' 13.83 USDT' in msg_mock.call_args_list[0][0][0]
|
||||||
assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0]
|
assert ' 15.21 USD' in msg_mock.call_args_list[0][0][0]
|
||||||
assert str(' 1 trade') in msg_mock.call_args_list[0][0][0]
|
assert '(2)' in msg_mock.call_args_list[0][0][0]
|
||||||
assert str(' 0 trade') 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
|
# Reset msg_mock
|
||||||
msg_mock.reset_mock()
|
msg_mock.reset_mock()
|
||||||
|
@ -458,32 +445,23 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee,
|
||||||
assert msg_mock.call_count == 1
|
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 "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(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((datetime.utcnow() - timedelta(days=5)).date()) in msg_mock.call_args_list[0][0][0]
|
||||||
assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0]
|
assert ' 13.83 USDT' in msg_mock.call_args_list[0][0][0]
|
||||||
assert str(' 1 trade') in msg_mock.call_args_list[0][0][0]
|
assert ' 15.21 USD' in msg_mock.call_args_list[0][0][0]
|
||||||
assert str(' 0 trade') 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
|
# Reset msg_mock
|
||||||
msg_mock.reset_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
|
# /daily 1
|
||||||
context = MagicMock()
|
context = MagicMock()
|
||||||
context.args = ["1"]
|
context.args = ["1"]
|
||||||
telegram._daily(update=update, context=context)
|
telegram._daily(update=update, context=context)
|
||||||
assert str(' 0.00018651 BTC') in msg_mock.call_args_list[0][0][0]
|
assert ' 13.83 USDT' in msg_mock.call_args_list[0][0][0]
|
||||||
assert str(' 2.798 USD') in msg_mock.call_args_list[0][0][0]
|
assert ' 15.21 USD' in msg_mock.call_args_list[0][0][0]
|
||||||
assert str(' 3 trades') 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:
|
def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None:
|
||||||
|
@ -512,15 +490,14 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None:
|
||||||
context = MagicMock()
|
context = MagicMock()
|
||||||
context.args = ["today"]
|
context.args = ["today"]
|
||||||
telegram._daily(update=update, context=context)
|
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,
|
def test_weekly_handle(default_conf_usdt, update, ticker, fee, mocker, time_machine) -> None:
|
||||||
limit_sell_order, mocker) -> None:
|
default_conf_usdt['max_open_trades'] = 1
|
||||||
default_conf['max_open_trades'] = 1
|
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
'freqtrade.rpc.rpc.CryptoToFiatConverter._find_price',
|
'freqtrade.rpc.rpc.CryptoToFiatConverter._find_price',
|
||||||
return_value=15000.0
|
return_value=1.1
|
||||||
)
|
)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
|
@ -528,25 +505,10 @@ def test_weekly_handle(default_conf, update, ticker, limit_buy_order, fee,
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
)
|
)
|
||||||
|
|
||||||
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
|
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf_usdt)
|
||||||
|
# Move to saturday - so all trades are within that week
|
||||||
patch_get_signal(freqtradebot)
|
time_machine.move_to('2022-06-11')
|
||||||
|
create_mock_trades_usdt(fee)
|
||||||
# Create some test data
|
|
||||||
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
|
|
||||||
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
|
|
||||||
|
|
||||||
# Try valid data
|
# Try valid data
|
||||||
# /weekly 2
|
# /weekly 2
|
||||||
|
@ -560,10 +522,10 @@ def test_weekly_handle(default_conf, update, ticker, limit_buy_order, fee,
|
||||||
today = datetime.utcnow().date()
|
today = datetime.utcnow().date()
|
||||||
first_iso_day_of_current_week = today - timedelta(days=today.weekday())
|
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(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 ' 9.83 USDT' in msg_mock.call_args_list[0][0][0]
|
||||||
assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0]
|
assert ' 10.81 USD' in msg_mock.call_args_list[0][0][0]
|
||||||
assert str(' 1 trade') in msg_mock.call_args_list[0][0][0]
|
assert '(3)' in msg_mock.call_args_list[0][0][0]
|
||||||
assert str(' 0 trade') in msg_mock.call_args_list[0][0][0]
|
assert '(0)' in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
# Reset msg_mock
|
# Reset msg_mock
|
||||||
msg_mock.reset_mock()
|
msg_mock.reset_mock()
|
||||||
|
@ -573,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>:" \
|
assert "Weekly Profit over the last 8 weeks (starting from Monday)</b>:" \
|
||||||
in msg_mock.call_args_list[0][0][0]
|
in msg_mock.call_args_list[0][0][0]
|
||||||
assert 'Weekly' 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 ' 9.83 USDT' in msg_mock.call_args_list[0][0][0]
|
||||||
assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0]
|
assert ' 10.81 USD' in msg_mock.call_args_list[0][0][0]
|
||||||
assert str(' 1 trade') in msg_mock.call_args_list[0][0][0]
|
assert '(3)' in msg_mock.call_args_list[0][0][0]
|
||||||
assert str(' 0 trade') 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
|
|
||||||
|
|
||||||
# /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)
|
|
||||||
|
|
||||||
# Try invalid data
|
# Try invalid data
|
||||||
msg_mock.reset_mock()
|
msg_mock.reset_mock()
|
||||||
|
@ -629,16 +557,17 @@ def test_weekly_wrong_input(default_conf, update, ticker, mocker) -> None:
|
||||||
context = MagicMock()
|
context = MagicMock()
|
||||||
context.args = ["this week"]
|
context.args = ["this week"]
|
||||||
telegram._weekly(update=update, context=context)
|
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]
|
in msg_mock.call_args_list[0][0][0]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_monthly_handle(default_conf, update, ticker, limit_buy_order, fee,
|
def test_monthly_handle(default_conf_usdt, update, ticker, fee, mocker, time_machine) -> None:
|
||||||
limit_sell_order, mocker) -> None:
|
default_conf_usdt['max_open_trades'] = 1
|
||||||
default_conf['max_open_trades'] = 1
|
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
'freqtrade.rpc.rpc.CryptoToFiatConverter._find_price',
|
'freqtrade.rpc.rpc.CryptoToFiatConverter._find_price',
|
||||||
return_value=15000.0
|
return_value=1.1
|
||||||
)
|
)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
|
@ -646,25 +575,10 @@ def test_monthly_handle(default_conf, update, ticker, limit_buy_order, fee,
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
)
|
)
|
||||||
|
|
||||||
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
|
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.
|
||||||
patch_get_signal(freqtradebot)
|
time_machine.move_to('2022-06-11')
|
||||||
|
create_mock_trades_usdt(fee)
|
||||||
# Create some test data
|
|
||||||
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
|
|
||||||
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
|
|
||||||
|
|
||||||
# Try valid data
|
# Try valid data
|
||||||
# /monthly 2
|
# /monthly 2
|
||||||
|
@ -677,10 +591,10 @@ def test_monthly_handle(default_conf, update, ticker, limit_buy_order, fee,
|
||||||
today = datetime.utcnow().date()
|
today = datetime.utcnow().date()
|
||||||
current_month = f"{today.year}-{today.month:02} "
|
current_month = f"{today.year}-{today.month:02} "
|
||||||
assert current_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 ' 9.83 USDT' in msg_mock.call_args_list[0][0][0]
|
||||||
assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0]
|
assert ' 10.81 USD' in msg_mock.call_args_list[0][0][0]
|
||||||
assert str(' 1 trade') in msg_mock.call_args_list[0][0][0]
|
assert '(3)' in msg_mock.call_args_list[0][0][0]
|
||||||
assert str(' 0 trade') in msg_mock.call_args_list[0][0][0]
|
assert '(0)' in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
# Reset msg_mock
|
# Reset msg_mock
|
||||||
msg_mock.reset_mock()
|
msg_mock.reset_mock()
|
||||||
|
@ -691,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 '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 'Month ' in msg_mock.call_args_list[0][0][0]
|
||||||
assert current_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 ' 9.83 USDT' in msg_mock.call_args_list[0][0][0]
|
||||||
assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0]
|
assert ' 10.81 USD' in msg_mock.call_args_list[0][0][0]
|
||||||
assert str(' 1 trade') in msg_mock.call_args_list[0][0][0]
|
assert '(3)' in msg_mock.call_args_list[0][0][0]
|
||||||
assert str(' 0 trade') in msg_mock.call_args_list[0][0][0]
|
assert '(0)' in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
# Reset msg_mock
|
# Reset msg_mock
|
||||||
msg_mock.reset_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
|
# /monthly 12
|
||||||
context = MagicMock()
|
context = MagicMock()
|
||||||
|
@ -716,24 +619,14 @@ def test_monthly_handle(default_conf, update, ticker, limit_buy_order, fee,
|
||||||
telegram._monthly(update=update, context=context)
|
telegram._monthly(update=update, context=context)
|
||||||
assert msg_mock.call_count == 1
|
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 '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 ' 9.83 USDT' in msg_mock.call_args_list[0][0][0]
|
||||||
assert str(' 2.798 USD') in msg_mock.call_args_list[0][0][0]
|
assert ' 10.81 USD' in msg_mock.call_args_list[0][0][0]
|
||||||
assert str(' 3 trades') 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"
|
# The one-digit months should contain a zero, Eg: September 2021 = "2021-09"
|
||||||
# Since we loaded the last 12 months, any month should appear
|
# Since we loaded the last 12 months, any month should appear
|
||||||
assert str('-09') in msg_mock.call_args_list[0][0][0]
|
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
|
# Try invalid data
|
||||||
msg_mock.reset_mock()
|
msg_mock.reset_mock()
|
||||||
freqtradebot.state = State.RUNNING
|
freqtradebot.state = State.RUNNING
|
||||||
|
@ -754,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]
|
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,
|
def test_profit_handle(default_conf_usdt, update, ticker_usdt, ticker_sell_up, fee,
|
||||||
limit_buy_order, limit_sell_order, mocker) -> None:
|
limit_sell_order_usdt, mocker) -> None:
|
||||||
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
|
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=1.1)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
fetch_ticker=ticker,
|
fetch_ticker=ticker_usdt,
|
||||||
get_fee=fee,
|
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)
|
patch_get_signal(freqtradebot)
|
||||||
|
|
||||||
telegram._profit(update=update, context=MagicMock())
|
telegram._profit(update=update, context=MagicMock())
|
||||||
|
@ -775,10 +668,6 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
|
||||||
freqtradebot.enter_positions()
|
freqtradebot.enter_positions()
|
||||||
trade = Trade.query.first()
|
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)
|
|
||||||
|
|
||||||
context = MagicMock()
|
context = MagicMock()
|
||||||
# Test with invalid 2nd argument (should silently pass)
|
# Test with invalid 2nd argument (should silently pass)
|
||||||
context.args = ["aaa"]
|
context.args = ["aaa"]
|
||||||
|
@ -786,15 +675,16 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
|
||||||
assert msg_mock.call_count == 1
|
assert msg_mock.call_count == 1
|
||||||
assert 'No closed trade' in msg_mock.call_args_list[-1][0][0]
|
assert 'No closed trade' in msg_mock.call_args_list[-1][0][0]
|
||||||
assert '*ROI:* All trades' in msg_mock.call_args_list[-1][0][0]
|
assert '*ROI:* All trades' in msg_mock.call_args_list[-1][0][0]
|
||||||
mocker.patch('freqtrade.wallets.Wallets.get_starting_balance', return_value=0.01)
|
mocker.patch('freqtrade.wallets.Wallets.get_starting_balance', return_value=1000)
|
||||||
assert ('∙ `-0.000005 BTC (-0.50%) (-0.0 \N{GREEK CAPITAL LETTER SIGMA}%)`'
|
assert ('∙ `0.298 USDT (0.50%) (0.03 \N{GREEK CAPITAL LETTER SIGMA}%)`'
|
||||||
in msg_mock.call_args_list[-1][0][0])
|
in msg_mock.call_args_list[-1][0][0])
|
||||||
msg_mock.reset_mock()
|
msg_mock.reset_mock()
|
||||||
|
|
||||||
# Update the ticker with a market going up
|
# Update the ticker with a market going up
|
||||||
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', ticker_sell_up)
|
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', ticker_sell_up)
|
||||||
# Simulate fulfilled LIMIT_SELL order for trade
|
# 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.update_trade(oobj)
|
||||||
|
|
||||||
trade.close_date = datetime.now(timezone.utc)
|
trade.close_date = datetime.now(timezone.utc)
|
||||||
|
@ -805,15 +695,15 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
|
||||||
telegram._profit(update=update, context=context)
|
telegram._profit(update=update, context=context)
|
||||||
assert msg_mock.call_count == 1
|
assert msg_mock.call_count == 1
|
||||||
assert '*ROI:* Closed trades' in msg_mock.call_args_list[-1][0][0]
|
assert '*ROI:* Closed trades' in msg_mock.call_args_list[-1][0][0]
|
||||||
assert ('∙ `0.00006217 BTC (6.20%) (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])
|
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 '*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])
|
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])
|
@pytest.mark.parametrize('is_short', [True, False])
|
||||||
|
@ -1350,71 +1240,43 @@ def test_force_enter_no_pair(default_conf, update, mocker) -> None:
|
||||||
assert fbuy_mock.call_count == 1
|
assert fbuy_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_telegram_performance_handle(default_conf, update, ticker, fee,
|
def test_telegram_performance_handle(default_conf_usdt, update, ticker, fee, mocker) -> None:
|
||||||
limit_buy_order, limit_sell_order, mocker) -> None:
|
|
||||||
|
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
fetch_ticker=ticker,
|
fetch_ticker=ticker,
|
||||||
get_fee=fee,
|
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
|
# Create some test data
|
||||||
freqtradebot.enter_positions()
|
create_mock_trades_usdt(fee)
|
||||||
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
|
|
||||||
telegram._performance(update=update, context=MagicMock())
|
telegram._performance(update=update, context=MagicMock())
|
||||||
assert msg_mock.call_count == 1
|
assert msg_mock.call_count == 1
|
||||||
assert 'Performance' in msg_mock.call_args_list[0][0][0]
|
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(
|
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(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
fetch_ticker=ticker,
|
fetch_ticker=ticker,
|
||||||
get_fee=fee,
|
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)
|
patch_get_signal(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)
|
|
||||||
|
|
||||||
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()
|
context = MagicMock()
|
||||||
telegram._enter_tag_performance(update=update, context=context)
|
telegram._enter_tag_performance(update=update, context=context)
|
||||||
assert msg_mock.call_count == 1
|
assert msg_mock.call_count == 1
|
||||||
assert 'Entry Tag Performance' in msg_mock.call_args_list[0][0][0]
|
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)
|
telegram._enter_tag_performance(update=update, context=context)
|
||||||
assert msg_mock.call_count == 2
|
assert msg_mock.call_count == 2
|
||||||
|
|
||||||
|
@ -1427,37 +1289,24 @@ def test_telegram_entry_tag_performance_handle(
|
||||||
assert "Error" in msg_mock.call_args_list[0][0][0]
|
assert "Error" in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
|
|
||||||
def test_telegram_exit_reason_performance_handle(default_conf, update, ticker, fee,
|
def test_telegram_exit_reason_performance_handle(default_conf_usdt, update, ticker, fee,
|
||||||
limit_buy_order, limit_sell_order, mocker) -> None:
|
mocker) -> None:
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
fetch_ticker=ticker,
|
fetch_ticker=ticker,
|
||||||
get_fee=fee,
|
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)
|
patch_get_signal(freqtradebot)
|
||||||
|
|
||||||
# Create some test data
|
create_mock_trades_usdt(fee)
|
||||||
freqtradebot.enter_positions()
|
|
||||||
trade = Trade.query.first()
|
|
||||||
assert trade
|
|
||||||
trade.exit_reason = 'TESTSELL'
|
|
||||||
# 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
|
|
||||||
context = MagicMock()
|
context = MagicMock()
|
||||||
telegram._exit_reason_performance(update=update, context=context)
|
telegram._exit_reason_performance(update=update, context=context)
|
||||||
assert msg_mock.call_count == 1
|
assert msg_mock.call_count == 1
|
||||||
assert 'Exit Reason Performance' in msg_mock.call_args_list[0][0][0]
|
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]
|
assert '<code>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._exit_reason_performance(update=update, context=context)
|
telegram._exit_reason_performance(update=update, context=context)
|
||||||
assert msg_mock.call_count == 2
|
assert msg_mock.call_count == 2
|
||||||
|
@ -1471,43 +1320,27 @@ def test_telegram_exit_reason_performance_handle(default_conf, update, ticker, f
|
||||||
assert "Error" in msg_mock.call_args_list[0][0][0]
|
assert "Error" in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
|
|
||||||
def test_telegram_mix_tag_performance_handle(default_conf, update, ticker, fee,
|
def test_telegram_mix_tag_performance_handle(default_conf_usdt, update, ticker, fee,
|
||||||
limit_buy_order, limit_sell_order, mocker) -> None:
|
mocker) -> None:
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
fetch_ticker=ticker,
|
fetch_ticker=ticker,
|
||||||
get_fee=fee,
|
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)
|
patch_get_signal(freqtradebot)
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
freqtradebot.enter_positions()
|
create_mock_trades_usdt(fee)
|
||||||
trade = Trade.query.first()
|
|
||||||
assert trade
|
|
||||||
|
|
||||||
trade.enter_tag = "TESTBUY"
|
|
||||||
trade.exit_reason = "TESTSELL"
|
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
context = MagicMock()
|
context = MagicMock()
|
||||||
telegram._mix_tag_performance(update=update, context=context)
|
telegram._mix_tag_performance(update=update, context=context)
|
||||||
assert msg_mock.call_count == 1
|
assert msg_mock.call_count == 1
|
||||||
assert 'Mix Tag Performance' in msg_mock.call_args_list[0][0][0]
|
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])
|
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)
|
telegram._mix_tag_performance(update=update, context=context)
|
||||||
assert msg_mock.call_count == 2
|
assert msg_mock.call_count == 2
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
# pragma pylint: disable=missing-docstring, C0103, protected-access
|
# pragma pylint: disable=missing-docstring, C0103, protected-access
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
@ -7,6 +8,7 @@ from requests import RequestException
|
||||||
|
|
||||||
from freqtrade.enums import ExitType, RPCMessageType
|
from freqtrade.enums import ExitType, RPCMessageType
|
||||||
from freqtrade.rpc import RPC
|
from freqtrade.rpc import RPC
|
||||||
|
from freqtrade.rpc.discord import Discord
|
||||||
from freqtrade.rpc.webhook import Webhook
|
from freqtrade.rpc.webhook import Webhook
|
||||||
from tests.conftest import get_patched_freqtradebot, log_has
|
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)
|
webhook._send_msg(msg)
|
||||||
|
|
||||||
assert post.call_args[1] == {'data': msg['data'], 'headers': {'Content-Type': 'text/plain'}}
|
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]
|
||||||
|
|
|
@ -20,7 +20,8 @@ from freqtrade.strategy.hyper import detect_parameters
|
||||||
from freqtrade.strategy.parameters import (BaseParameter, BooleanParameter, CategoricalParameter,
|
from freqtrade.strategy.parameters import (BaseParameter, BooleanParameter, CategoricalParameter,
|
||||||
DecimalParameter, IntParameter, RealParameter)
|
DecimalParameter, IntParameter, RealParameter)
|
||||||
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
|
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
|
from .strats.strategy_test_v3 import StrategyTestV3
|
||||||
|
|
||||||
|
@ -812,6 +813,28 @@ def test_strategy_safe_wrapper(value):
|
||||||
assert ret == 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():
|
def test_hyperopt_parameters():
|
||||||
from skopt.space import Categorical, Integer, Real
|
from skopt.space import Categorical, Integer, Real
|
||||||
with pytest.raises(OperationalException, match=r"Name is determined.*"):
|
with pytest.raises(OperationalException, match=r"Name is determined.*"):
|
||||||
|
|
|
@ -210,13 +210,14 @@ def test_edge_overrides_stoploss(limit_order, fee, caplog, mocker,
|
||||||
#
|
#
|
||||||
# mocking the ticker: price is falling ...
|
# mocking the ticker: price is falling ...
|
||||||
enter_price = limit_order['buy']['price']
|
enter_price = limit_order['buy']['price']
|
||||||
|
ticker_val = {
|
||||||
|
'bid': enter_price,
|
||||||
|
'ask': enter_price,
|
||||||
|
'last': enter_price,
|
||||||
|
}
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
fetch_ticker=MagicMock(return_value={
|
fetch_ticker=MagicMock(return_value=ticker_val),
|
||||||
'bid': enter_price * buy_price_mult,
|
|
||||||
'ask': enter_price * buy_price_mult,
|
|
||||||
'last': enter_price * buy_price_mult,
|
|
||||||
}),
|
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
)
|
)
|
||||||
#############################################
|
#############################################
|
||||||
|
@ -229,9 +230,12 @@ def test_edge_overrides_stoploss(limit_order, fee, caplog, mocker,
|
||||||
freqtrade.enter_positions()
|
freqtrade.enter_positions()
|
||||||
trade = Trade.query.first()
|
trade = Trade.query.first()
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
oobj = Order.parse_from_ccxt_object(limit_order['buy'], 'ADA/USDT', '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
|
# stoploss shoud be hit
|
||||||
assert freqtrade.handle_trade(trade) is not ignore_strat_sl
|
assert freqtrade.handle_trade(trade) is not ignore_strat_sl
|
||||||
|
@ -3771,6 +3775,7 @@ def test_exit_profit_only(
|
||||||
trade = Trade.query.first()
|
trade = Trade.query.first()
|
||||||
assert trade.is_short == is_short
|
assert trade.is_short == is_short
|
||||||
oobj = Order.parse_from_ccxt_object(limit_order[eside], limit_order[eside]['symbol'], eside)
|
oobj = Order.parse_from_ccxt_object(limit_order[eside], limit_order[eside]['symbol'], eside)
|
||||||
|
trade.update_order(limit_order[eside])
|
||||||
trade.update_trade(oobj)
|
trade.update_trade(oobj)
|
||||||
freqtrade.wallets.update()
|
freqtrade.wallets.update()
|
||||||
if profit_only:
|
if profit_only:
|
||||||
|
@ -4059,6 +4064,7 @@ def test_trailing_stop_loss_positive(
|
||||||
trade = Trade.query.first()
|
trade = Trade.query.first()
|
||||||
assert trade.is_short == is_short
|
assert trade.is_short == is_short
|
||||||
oobj = Order.parse_from_ccxt_object(limit_order[eside], limit_order[eside]['symbol'], eside)
|
oobj = Order.parse_from_ccxt_object(limit_order[eside], limit_order[eside]['symbol'], eside)
|
||||||
|
trade.update_order(limit_order[eside])
|
||||||
trade.update_trade(oobj)
|
trade.update_trade(oobj)
|
||||||
caplog.set_level(logging.DEBUG)
|
caplog.set_level(logging.DEBUG)
|
||||||
# stop-loss not reached
|
# stop-loss not reached
|
||||||
|
|
Loading…
Reference in New Issue
Block a user