diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 402e5641d..2d1fb20fc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,7 +18,7 @@ repos: - types-requests==2.28.11.15 - types-tabulate==0.9.0.1 - types-python-dateutil==2.8.19.10 - - SQLAlchemy==2.0.4 + - SQLAlchemy==2.0.5.post1 # stages: [push] - repo: https://github.com/pycqa/isort diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 065411018..1b9a1f9b7 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,6 +1,6 @@ markdown==3.3.7 mkdocs==1.4.2 -mkdocs-material==9.0.15 +mkdocs-material==9.1.1 mdx_truly_sane_lists==1.3 -pymdown-extensions==9.9.2 +pymdown-extensions==9.10 jinja2==3.1.2 diff --git a/freqtrade/persistence/pairlock.py b/freqtrade/persistence/pairlock.py index a6d1eeaf0..1e5699145 100644 --- a/freqtrade/persistence/pairlock.py +++ b/freqtrade/persistence/pairlock.py @@ -2,8 +2,7 @@ from datetime import datetime, timezone from typing import Any, ClassVar, Dict, Optional from sqlalchemy import String, or_ -from sqlalchemy.orm import Mapped, Query, mapped_column -from sqlalchemy.orm.scoping import _QueryDescriptorType +from sqlalchemy.orm import Mapped, Query, QueryPropertyDescriptor, mapped_column from freqtrade.constants import DATETIME_PRINT_FORMAT from freqtrade.persistence.base import ModelBase, SessionType @@ -14,7 +13,7 @@ class PairLock(ModelBase): Pair Locks database model. """ __tablename__ = 'pairlocks' - query: ClassVar[_QueryDescriptorType] + query: ClassVar[QueryPropertyDescriptor] _session: ClassVar[SessionType] id: Mapped[int] = mapped_column(primary_key=True) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 21fe80819..8e8a414c8 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -8,8 +8,8 @@ from math import isclose from typing import Any, ClassVar, Dict, List, Optional, cast from sqlalchemy import Enum, Float, ForeignKey, Integer, String, UniqueConstraint, desc, func -from sqlalchemy.orm import Mapped, Query, lazyload, mapped_column, relationship -from sqlalchemy.orm.scoping import _QueryDescriptorType +from sqlalchemy.orm import (Mapped, Query, QueryPropertyDescriptor, lazyload, mapped_column, + relationship) from freqtrade.constants import (DATETIME_PRINT_FORMAT, MATH_CLOSE_PREC, NON_OPEN_EXCHANGE_STATES, BuySell, LongShort) @@ -36,7 +36,7 @@ class Order(ModelBase): Mirrors CCXT Order structure """ __tablename__ = 'orders' - query: ClassVar[_QueryDescriptorType] + query: ClassVar[QueryPropertyDescriptor] _session: ClassVar[SessionType] # Uniqueness should be ensured over pair, order_id @@ -1181,7 +1181,7 @@ class Trade(ModelBase, LocalTrade): Note: Fields must be aligned with LocalTrade class """ __tablename__ = 'trades' - query: ClassVar[_QueryDescriptorType] + query: ClassVar[QueryPropertyDescriptor] _session: ClassVar[SessionType] use_db: bool = True diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index a751179b2..064a509fd 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -286,6 +286,7 @@ class OpenTradeSchema(TradeSchema): current_rate: float total_profit_abs: float total_profit_fiat: Optional[float] + total_profit_ratio: Optional[float] open_order: Optional[str] diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index f6bab3624..8ea70bb69 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -42,7 +42,8 @@ logger = logging.getLogger(__name__) # 2.22: Add FreqAI to backtesting # 2.23: Allow plot config request in webserver mode # 2.24: Add cancel_open_order endpoint -API_VERSION = 2.24 +# 2.25: Add several profit values to /status endpoint +API_VERSION = 2.25 # Public API, requires no auth. router_public = APIRouter() diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 8692c477f..c68ed2d48 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -192,6 +192,11 @@ class RPC: current_profit = trade.close_profit or 0.0 current_profit_abs = trade.close_profit_abs or 0.0 total_profit_abs = trade.realized_profit + current_profit_abs + total_profit_ratio: Optional[float] = None + if trade.max_stake_amount: + total_profit_ratio = ( + (total_profit_abs / trade.max_stake_amount) * trade.leverage + ) # Calculate fiat profit if not isnan(current_profit_abs) and self._fiat_converter: @@ -224,6 +229,7 @@ class RPC: total_profit_abs=total_profit_abs, total_profit_fiat=total_profit_fiat, + total_profit_ratio=total_profit_ratio, stoploss_current_dist=stoploss_current_dist, stoploss_current_dist_ratio=round(stoploss_current_dist_ratio, 8), stoploss_current_dist_pct=round(stoploss_current_dist_ratio * 100, 2), diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 1a96b1671..30aa55359 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -510,14 +510,14 @@ class Telegram(RPCHandler): if prev_avg_price: minus_on_entry = (cur_entry_average - prev_avg_price) / prev_avg_price - lines.append(f"*{wording} #{order_nr}:* at {minus_on_entry:.2%} avg profit") + lines.append(f"*{wording} #{order_nr}:* at {minus_on_entry:.2%} avg Profit") if is_open: lines.append("({})".format(cur_entry_datetime .humanize(granularity=["day", "hour", "minute"]))) lines.append(f"*Amount:* {cur_entry_amount} " f"({round_coin_value(order['cost'], quote_currency)})") lines.append(f"*Average {wording} Price:* {cur_entry_average} " - f"({price_to_1st_entry:.2%} from 1st entry rate)") + f"({price_to_1st_entry:.2%} from 1st entry Rate)") lines.append(f"*Order filled:* {order['order_filled_date']}") # TODO: is this really useful? @@ -569,6 +569,8 @@ class Telegram(RPCHandler): and not o['ft_order_side'] == 'stoploss']) r['exit_reason'] = r.get('exit_reason', "") r['stake_amount_r'] = round_coin_value(r['stake_amount'], r['quote_currency']) + r['max_stake_amount_r'] = round_coin_value( + r['max_stake_amount'] or r['stake_amount'], r['quote_currency']) r['profit_abs_r'] = round_coin_value(r['profit_abs'], r['quote_currency']) r['realized_profit_r'] = round_coin_value(r['realized_profit'], r['quote_currency']) r['total_profit_abs_r'] = round_coin_value( @@ -580,31 +582,37 @@ class Telegram(RPCHandler): f"*Direction:* {'`Short`' if r.get('is_short') else '`Long`'}" + " ` ({leverage}x)`" if r.get('leverage') else "", "*Amount:* `{amount} ({stake_amount_r})`", + "*Total invested:* `{max_stake_amount_r}`" if position_adjust else "", "*Enter Tag:* `{enter_tag}`" if r['enter_tag'] else "", "*Exit Reason:* `{exit_reason}`" if r['exit_reason'] else "", ] if position_adjust: max_buy_str = (f"/{max_entries + 1}" if (max_entries > 0) else "") - lines.append("*Number of Entries:* `{num_entries}" + max_buy_str + "`") - lines.append("*Number of Exits:* `{num_exits}`") + lines.extend([ + "*Number of Entries:* `{num_entries}" + max_buy_str + "`", + "*Number of Exits:* `{num_exits}`" + ]) lines.extend([ "*Open Rate:* `{open_rate:.8f}`", "*Close Rate:* `{close_rate:.8f}`" if r['close_rate'] else "", "*Open Date:* `{open_date}`", "*Close Date:* `{close_date}`" if r['close_date'] else "", - "*Current Rate:* `{current_rate:.8f}`" if r['is_open'] else "", + " \n*Current Rate:* `{current_rate:.8f}`" if r['is_open'] else "", ("*Unrealized Profit:* " if r['is_open'] else "*Close Profit: *") + "`{profit_ratio:.2%}` `({profit_abs_r})`", ]) if r['is_open']: if r.get('realized_profit'): - lines.append( - "*Realized Profit:* `{realized_profit_r} {realized_profit_ratio:.2%}`") - lines.append("*Total Profit:* `{total_profit_abs_r}` ") + lines.extend([ + "*Realized Profit:* `{realized_profit_ratio:.2%} ({realized_profit_r})`", + "*Total Profit:* `{total_profit_ratio:.2%} ({total_profit_abs_r})`" + ]) + # Append empty line to improve readability + lines.append(" ") if (r['stop_loss_abs'] != r['initial_stop_loss_abs'] and r['initial_stop_loss_ratio'] is not None): # Adding initial stoploss only if it is different from stoploss diff --git a/requirements-dev.txt b/requirements-dev.txt index a945ffc63..aa6012b1d 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,7 +7,7 @@ -r docs/requirements-docs.txt coveralls==3.3.1 -ruff==0.0.253 +ruff==0.0.254 mypy==1.0.1 pre-commit==3.1.1 pytest==7.2.1 diff --git a/requirements.txt b/requirements.txt index a6b3ddd51..a702507f9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,9 +3,9 @@ pandas==1.5.3 pandas-ta==0.3.14b ccxt==2.8.98 -cryptography==39.0.1 +cryptography==39.0.2 aiohttp==3.8.4 -SQLAlchemy==2.0.4 +SQLAlchemy==2.0.5.post1 python-telegram-bot==13.15 arrow==1.2.3 cachetools==4.2.2 @@ -28,7 +28,7 @@ py_find_1st==1.1.5 # Load ticker files 30% faster python-rapidjson==1.9 # Properly format api responses -orjson==3.8.6 +orjson==3.8.7 # Notify systemd sdnotify==0.3.2 @@ -45,7 +45,7 @@ psutil==5.9.4 colorama==0.4.6 # Building config files interactively questionary==1.10.0 -prompt-toolkit==3.0.37 +prompt-toolkit==3.0.38 # Extensions to datetime library python-dateutil==2.8.2 diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index cd72da763..1a1802c68 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -50,7 +50,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'amount': 91.07468123, 'amount_requested': 91.07468124, 'stake_amount': 0.001, - 'max_stake_amount': ANY, + 'max_stake_amount': None, 'trade_duration': None, 'trade_duration_s': None, 'close_profit': None, @@ -79,6 +79,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'realized_profit_ratio': None, 'total_profit_abs': -4.09e-06, 'total_profit_fiat': ANY, + 'total_profit_ratio': None, 'exchange': 'binance', 'leverage': 1.0, 'interest_rate': 0.0, @@ -168,6 +169,10 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: results = rpc._rpc_trade_status() response = deepcopy(gen_response) + response.update({ + 'max_stake_amount': 0.001, + 'total_profit_ratio': pytest.approx(-0.00409), + }) assert results[0] == response mocker.patch(f'{EXMS}.get_rate', @@ -181,10 +186,12 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'stoploss_current_dist': ANY, 'stoploss_current_dist_ratio': ANY, 'stoploss_current_dist_pct': ANY, + 'max_stake_amount': 0.001, 'profit_ratio': ANY, 'profit_pct': ANY, 'profit_abs': ANY, 'total_profit_abs': ANY, + 'total_profit_ratio': ANY, 'current_rate': ANY, }) assert results[0] == response_norate diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index e140a43f1..9c2c3ee3a 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -1012,6 +1012,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets, is_short, 'profit_fiat': ANY, 'total_profit_abs': ANY, 'total_profit_fiat': ANY, + 'total_profit_ratio': ANY, 'realized_profit': 0.0, 'realized_profit_ratio': None, 'current_rate': current_rate, diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 69d0f805d..1dc255b3e 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -198,6 +198,7 @@ def test_telegram_status(default_conf, update, mocker) -> None: 'current_rate': 1.098e-05, 'amount': 90.99181074, 'stake_amount': 90.99181074, + 'max_stake_amount': 90.99181074, 'buy_tag': None, 'enter_tag': None, 'close_profit_ratio': None,