From c083723698e4894a92109ffc8977a83987ecc3cb Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 8 Apr 2023 10:02:38 +0200 Subject: [PATCH 01/10] Add initial version of key value store --- freqtrade/persistence/__init__.py | 1 + freqtrade/persistence/key_value_store.py | 97 ++++++++++++++++++++++++ freqtrade/persistence/models.py | 2 + 3 files changed, 100 insertions(+) create mode 100644 freqtrade/persistence/key_value_store.py diff --git a/freqtrade/persistence/__init__.py b/freqtrade/persistence/__init__.py index 9e1a7e922..c69a54b7f 100644 --- a/freqtrade/persistence/__init__.py +++ b/freqtrade/persistence/__init__.py @@ -1,5 +1,6 @@ # flake8: noqa: F401 +from freqtrade.persistence.key_value_store import KeyValueStore from freqtrade.persistence.models import init_db from freqtrade.persistence.pairlock_middleware import PairLocks from freqtrade.persistence.trade_model import LocalTrade, Order, Trade diff --git a/freqtrade/persistence/key_value_store.py b/freqtrade/persistence/key_value_store.py new file mode 100644 index 000000000..5ad98d69d --- /dev/null +++ b/freqtrade/persistence/key_value_store.py @@ -0,0 +1,97 @@ +from datetime import datetime, timezone +from enum import Enum +from typing import ClassVar, Optional, Union + +from sqlalchemy import String +from sqlalchemy.orm import Mapped, mapped_column + +from freqtrade.persistence.base import ModelBase, SessionType + + +ValueTypes = Union[str, datetime, float, int] + + +class ValueTypesEnum(str, Enum): + STRING = 'str' + DATETIME = 'datetime' + FLOAT = 'float' + INT = 'int' + + +class _KeyValueStoreModel(ModelBase): + """ + Pair Locks database model. + """ + __tablename__ = 'KeyValueStore' + session: ClassVar[SessionType] + + id: Mapped[int] = mapped_column(primary_key=True) + + key: Mapped[str] = mapped_column(String(25), nullable=False, index=True) + + value_type: Mapped[ValueTypesEnum] = mapped_column(String(25), nullable=False) + + string_value: Mapped[Optional[str]] + datetime_value: Mapped[Optional[datetime]] + float_value: Mapped[Optional[float]] + int_value: Mapped[Optional[int]] + + +class KeyValueStore(): + + @staticmethod + def get_value(key: str) -> Optional[ValueTypes]: + """ + Get the value for the given key. + """ + kv = _KeyValueStoreModel.session.query(_KeyValueStoreModel).filter( + _KeyValueStoreModel.key == key).first() + if kv is None: + return None + if kv.value_type == ValueTypesEnum.STRING: + return kv.string_value + if kv.value_type == ValueTypesEnum.DATETIME and kv.datetime_value is not None: + return kv.datetime_value.replace(tzinfo=timezone.utc) + if kv.value_type == ValueTypesEnum.FLOAT: + return kv.float_value + if kv.value_type == ValueTypesEnum.INT: + return kv.int_value + # This should never happen unless someone messed with the database manually + raise ValueError(f'Unknown value type {kv.value_type}') # pragma: no cover + + @staticmethod + def store_value(key: str, value: ValueTypes) -> None: + """ + Store the given value for the given key. + """ + kv = _KeyValueStoreModel.session.query(_KeyValueStoreModel).filter( + _KeyValueStoreModel.key == key).first() + if kv is None: + kv = _KeyValueStoreModel(key=key) + if isinstance(value, str): + kv.value_type = ValueTypesEnum.STRING + kv.string_value = value + elif isinstance(value, datetime): + kv.value_type = ValueTypesEnum.DATETIME + kv.datetime_value = value + elif isinstance(value, float): + kv.value_type = ValueTypesEnum.FLOAT + kv.float_value = value + elif isinstance(value, int): + kv.value_type = ValueTypesEnum.INT + kv.int_value = value + else: + raise ValueError(f'Unknown value type {kv.value_type}') + _KeyValueStoreModel.session.add(kv) + _KeyValueStoreModel.session.commit() + + @staticmethod + def delete_value(key: str) -> None: + """ + Delete the value for the given key. + """ + kv = _KeyValueStoreModel.session.query(_KeyValueStoreModel).filter( + _KeyValueStoreModel.key == key).first() + if kv is not None: + _KeyValueStoreModel.session.delete(kv) + _KeyValueStoreModel.session.commit() diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 2315c0acc..e561e727b 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -13,6 +13,7 @@ from sqlalchemy.pool import StaticPool from freqtrade.exceptions import OperationalException from freqtrade.persistence.base import ModelBase +from freqtrade.persistence.key_value_store import _KeyValueStoreModel from freqtrade.persistence.migrations import check_migrate from freqtrade.persistence.pairlock import PairLock from freqtrade.persistence.trade_model import Order, Trade @@ -76,6 +77,7 @@ def init_db(db_url: str) -> None: bind=engine, autoflush=False), scopefunc=get_request_or_thread_id) Order.session = Trade.session PairLock.session = Trade.session + _KeyValueStoreModel.session = Trade.session previous_tables = inspect(engine).get_table_names() ModelBase.metadata.create_all(engine) From 4d4f4bf23ea0fa8aff1888b1fa41ca22ee0adec6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 8 Apr 2023 10:07:21 +0200 Subject: [PATCH 02/10] Add test for key_value_store --- tests/persistence/test_key_value_store.py | 37 +++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 tests/persistence/test_key_value_store.py diff --git a/tests/persistence/test_key_value_store.py b/tests/persistence/test_key_value_store.py new file mode 100644 index 000000000..27e56ba11 --- /dev/null +++ b/tests/persistence/test_key_value_store.py @@ -0,0 +1,37 @@ +from datetime import datetime, timedelta, timezone + +import pytest + +from freqtrade.persistence.key_value_store import KeyValueStore + + +@pytest.mark.usefixtures("init_persistence") +def test_key_value_store(time_machine): + start = datetime(2023, 1, 1, 4, tzinfo=timezone.utc) + time_machine.move_to(start, tick=False) + + KeyValueStore.store_value("test", "testStringValue") + KeyValueStore.store_value("test_dt", datetime.now(timezone.utc)) + KeyValueStore.store_value("test_float", 22.51) + KeyValueStore.store_value("test_int", 15) + + assert KeyValueStore.get_value("test") == "testStringValue" + assert KeyValueStore.get_value("test_dt") == datetime.now(timezone.utc) + assert KeyValueStore.get_value("test_float") == 22.51 + assert KeyValueStore.get_value("test_int") == 15 + + time_machine.move_to(start + timedelta(days=20, hours=5), tick=False) + assert KeyValueStore.get_value("test_dt") != datetime.now(timezone.utc) + assert KeyValueStore.get_value("test_dt") == start + # Test update works + KeyValueStore.store_value("test_dt", datetime.now(timezone.utc)) + assert KeyValueStore.get_value("test_dt") == datetime.now(timezone.utc) + + KeyValueStore.store_value("test_float", 23.51) + assert KeyValueStore.get_value("test_float") == 23.51 + # test deleting + KeyValueStore.delete_value("test_float") + assert KeyValueStore.get_value("test_float") is None + + with pytest.raises(ValueError, match=r"Unknown value type"): + KeyValueStore.store_value("test_float", {'some': 'dict'}) From ac817b7808ddf0559d2bb7d8142cdade5434af11 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 8 Apr 2023 10:09:31 +0200 Subject: [PATCH 03/10] Improve docstrings for key-value store --- freqtrade/persistence/key_value_store.py | 9 +++++++++ tests/persistence/test_key_value_store.py | 2 ++ 2 files changed, 11 insertions(+) diff --git a/freqtrade/persistence/key_value_store.py b/freqtrade/persistence/key_value_store.py index 5ad98d69d..109f94fcc 100644 --- a/freqtrade/persistence/key_value_store.py +++ b/freqtrade/persistence/key_value_store.py @@ -38,11 +38,17 @@ class _KeyValueStoreModel(ModelBase): class KeyValueStore(): + """ + Generic bot-wide, persistent key-value store + Can be used to store generic values, e.g. very first bot startup time. + Supports the types str, datetime, float and int. + """ @staticmethod def get_value(key: str) -> Optional[ValueTypes]: """ Get the value for the given key. + :param key: Key to get the value for """ kv = _KeyValueStoreModel.session.query(_KeyValueStoreModel).filter( _KeyValueStoreModel.key == key).first() @@ -63,6 +69,8 @@ class KeyValueStore(): def store_value(key: str, value: ValueTypes) -> None: """ Store the given value for the given key. + :param key: Key to store the value for - can be used in get-value to retrieve the key + :param value: Value to store - can be str, datetime, float or int """ kv = _KeyValueStoreModel.session.query(_KeyValueStoreModel).filter( _KeyValueStoreModel.key == key).first() @@ -89,6 +97,7 @@ class KeyValueStore(): def delete_value(key: str) -> None: """ Delete the value for the given key. + :param key: Key to delete the value for """ kv = _KeyValueStoreModel.session.query(_KeyValueStoreModel).filter( _KeyValueStoreModel.key == key).first() diff --git a/tests/persistence/test_key_value_store.py b/tests/persistence/test_key_value_store.py index 27e56ba11..8da5e4659 100644 --- a/tests/persistence/test_key_value_store.py +++ b/tests/persistence/test_key_value_store.py @@ -32,6 +32,8 @@ def test_key_value_store(time_machine): # test deleting KeyValueStore.delete_value("test_float") assert KeyValueStore.get_value("test_float") is None + # Delete same value again (should not fail) + KeyValueStore.delete_value("test_float") with pytest.raises(ValueError, match=r"Unknown value type"): KeyValueStore.store_value("test_float", {'some': 'dict'}) From 7751768b2e1131b837e95352228281e2f6f8290e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 8 Apr 2023 14:30:27 +0200 Subject: [PATCH 04/10] Store initial_time value --- freqtrade/freqtradebot.py | 2 ++ freqtrade/persistence/key_value_store.py | 16 ++++++++++++++++ tests/persistence/test_key_value_store.py | 23 ++++++++++++++++++++++- 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 48e3ec209..c4ef7794f 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -26,6 +26,7 @@ from freqtrade.exchange import (ROUND_DOWN, ROUND_UP, timeframe_to_minutes, time from freqtrade.misc import safe_value_fallback, safe_value_fallback2 from freqtrade.mixins import LoggingMixin from freqtrade.persistence import Order, PairLocks, Trade, init_db +from freqtrade.persistence.key_value_store import KeyValueStore, set_startup_time from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.plugins.protectionmanager import ProtectionManager from freqtrade.resolvers import ExchangeResolver, StrategyResolver @@ -182,6 +183,7 @@ class FreqtradeBot(LoggingMixin): performs startup tasks """ migrate_binance_futures_names(self.config) + set_startup_time() self.rpc.startup_messages(self.config, self.pairlists, self.protections) # Update older trades with precision and precision mode diff --git a/freqtrade/persistence/key_value_store.py b/freqtrade/persistence/key_value_store.py index 109f94fcc..0fdfc5aa6 100644 --- a/freqtrade/persistence/key_value_store.py +++ b/freqtrade/persistence/key_value_store.py @@ -104,3 +104,19 @@ class KeyValueStore(): if kv is not None: _KeyValueStoreModel.session.delete(kv) _KeyValueStoreModel.session.commit() + + +def set_startup_time(): + """ + sets bot_start_time to the first trade open date - or "now" on new databases. + sets startup_time to "now" + """ + st = KeyValueStore.get_value('bot_start_time') + if st is None: + from freqtrade.persistence import Trade + t = Trade.session.query(Trade).order_by(Trade.open_date.asc()).first() + if t is not None: + KeyValueStore.store_value('bot_start_time', t.open_date_utc) + else: + KeyValueStore.store_value('bot_start_time', datetime.now(timezone.utc)) + KeyValueStore.store_value('startup_time', datetime.now(timezone.utc)) diff --git a/tests/persistence/test_key_value_store.py b/tests/persistence/test_key_value_store.py index 8da5e4659..6113c57fa 100644 --- a/tests/persistence/test_key_value_store.py +++ b/tests/persistence/test_key_value_store.py @@ -2,7 +2,8 @@ from datetime import datetime, timedelta, timezone import pytest -from freqtrade.persistence.key_value_store import KeyValueStore +from freqtrade.persistence.key_value_store import KeyValueStore, set_startup_time +from tests.conftest import create_mock_trades_usdt @pytest.mark.usefixtures("init_persistence") @@ -37,3 +38,23 @@ def test_key_value_store(time_machine): with pytest.raises(ValueError, match=r"Unknown value type"): KeyValueStore.store_value("test_float", {'some': 'dict'}) + + +@pytest.mark.usefixtures("init_persistence") +def test_set_startup_time(fee, time_machine): + create_mock_trades_usdt(fee) + start = datetime.now(timezone.utc) + time_machine.move_to(start, tick=False) + set_startup_time() + + assert KeyValueStore.get_value("startup_time") == start + initial_time = KeyValueStore.get_value("bot_start_time") + assert initial_time <= start + + # Simulate bot restart + new_start = start + timedelta(days=5) + time_machine.move_to(new_start, tick=False) + set_startup_time() + + assert KeyValueStore.get_value("startup_time") == new_start + assert KeyValueStore.get_value("bot_start_time") == initial_time From 7ff30c6df8746618c0f0116bbf7fbc7fe4a39465 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 8 Apr 2023 16:23:55 +0200 Subject: [PATCH 05/10] Add additional, typesafe getters --- freqtrade/persistence/key_value_store.py | 94 ++++++++++++++++++----- tests/persistence/test_key_value_store.py | 9 +++ 2 files changed, 82 insertions(+), 21 deletions(-) diff --git a/freqtrade/persistence/key_value_store.py b/freqtrade/persistence/key_value_store.py index 0fdfc5aa6..ddf94bfbf 100644 --- a/freqtrade/persistence/key_value_store.py +++ b/freqtrade/persistence/key_value_store.py @@ -44,27 +44,6 @@ class KeyValueStore(): Supports the types str, datetime, float and int. """ - @staticmethod - def get_value(key: str) -> Optional[ValueTypes]: - """ - Get the value for the given key. - :param key: Key to get the value for - """ - kv = _KeyValueStoreModel.session.query(_KeyValueStoreModel).filter( - _KeyValueStoreModel.key == key).first() - if kv is None: - return None - if kv.value_type == ValueTypesEnum.STRING: - return kv.string_value - if kv.value_type == ValueTypesEnum.DATETIME and kv.datetime_value is not None: - return kv.datetime_value.replace(tzinfo=timezone.utc) - if kv.value_type == ValueTypesEnum.FLOAT: - return kv.float_value - if kv.value_type == ValueTypesEnum.INT: - return kv.int_value - # This should never happen unless someone messed with the database manually - raise ValueError(f'Unknown value type {kv.value_type}') # pragma: no cover - @staticmethod def store_value(key: str, value: ValueTypes) -> None: """ @@ -105,6 +84,79 @@ class KeyValueStore(): _KeyValueStoreModel.session.delete(kv) _KeyValueStoreModel.session.commit() + @staticmethod + def get_value(key: str) -> Optional[ValueTypes]: + """ + Get the value for the given key. + :param key: Key to get the value for + """ + kv = _KeyValueStoreModel.session.query(_KeyValueStoreModel).filter( + _KeyValueStoreModel.key == key).first() + if kv is None: + return None + if kv.value_type == ValueTypesEnum.STRING: + return kv.string_value + if kv.value_type == ValueTypesEnum.DATETIME and kv.datetime_value is not None: + return kv.datetime_value.replace(tzinfo=timezone.utc) + if kv.value_type == ValueTypesEnum.FLOAT: + return kv.float_value + if kv.value_type == ValueTypesEnum.INT: + return kv.int_value + # This should never happen unless someone messed with the database manually + raise ValueError(f'Unknown value type {kv.value_type}') # pragma: no cover + + @staticmethod + def get_string_value(key: str) -> Optional[str]: + """ + Get the value for the given key. + :param key: Key to get the value for + """ + kv = _KeyValueStoreModel.session.query(_KeyValueStoreModel).filter( + _KeyValueStoreModel.key == key, + _KeyValueStoreModel.value_type == ValueTypesEnum.STRING).first() + if kv is None: + return None + return kv.string_value + + @staticmethod + def get_datetime_value(key: str) -> Optional[datetime]: + """ + Get the value for the given key. + :param key: Key to get the value for + """ + kv = _KeyValueStoreModel.session.query(_KeyValueStoreModel).filter( + _KeyValueStoreModel.key == key, + _KeyValueStoreModel.value_type == ValueTypesEnum.DATETIME).first() + if kv is None or kv.datetime_value is None: + return None + return kv.datetime_value.replace(tzinfo=timezone.utc) + + @staticmethod + def get_float_value(key: str) -> Optional[float]: + """ + Get the value for the given key. + :param key: Key to get the value for + """ + kv = _KeyValueStoreModel.session.query(_KeyValueStoreModel).filter( + _KeyValueStoreModel.key == key, + _KeyValueStoreModel.value_type == ValueTypesEnum.FLOAT).first() + if kv is None: + return None + return kv.float_value + + @staticmethod + def get_int_value(key: str) -> Optional[int]: + """ + Get the value for the given key. + :param key: Key to get the value for + """ + kv = _KeyValueStoreModel.session.query(_KeyValueStoreModel).filter( + _KeyValueStoreModel.key == key, + _KeyValueStoreModel.value_type == ValueTypesEnum.INT).first() + if kv is None: + return None + return kv.int_value + def set_startup_time(): """ diff --git a/tests/persistence/test_key_value_store.py b/tests/persistence/test_key_value_store.py index 6113c57fa..1dab8764a 100644 --- a/tests/persistence/test_key_value_store.py +++ b/tests/persistence/test_key_value_store.py @@ -17,9 +17,18 @@ def test_key_value_store(time_machine): KeyValueStore.store_value("test_int", 15) assert KeyValueStore.get_value("test") == "testStringValue" + assert KeyValueStore.get_value("test") == "testStringValue" + assert KeyValueStore.get_string_value("test") == "testStringValue" assert KeyValueStore.get_value("test_dt") == datetime.now(timezone.utc) + assert KeyValueStore.get_datetime_value("test_dt") == datetime.now(timezone.utc) + assert KeyValueStore.get_string_value("test_dt") is None + assert KeyValueStore.get_float_value("test_dt") is None + assert KeyValueStore.get_int_value("test_dt") is None assert KeyValueStore.get_value("test_float") == 22.51 + assert KeyValueStore.get_float_value("test_float") == 22.51 assert KeyValueStore.get_value("test_int") == 15 + assert KeyValueStore.get_int_value("test_int") == 15 + assert KeyValueStore.get_datetime_value("test_int") is None time_machine.move_to(start + timedelta(days=20, hours=5), tick=False) assert KeyValueStore.get_value("test_dt") != datetime.now(timezone.utc) From fa3a81b02211a72d073e0fd8f6f2cdd261d6a4d5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 8 Apr 2023 16:28:50 +0200 Subject: [PATCH 06/10] convert Keys to enum --- freqtrade/persistence/key_value_store.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/freqtrade/persistence/key_value_store.py b/freqtrade/persistence/key_value_store.py index ddf94bfbf..2d26acbd3 100644 --- a/freqtrade/persistence/key_value_store.py +++ b/freqtrade/persistence/key_value_store.py @@ -18,6 +18,11 @@ class ValueTypesEnum(str, Enum): INT = 'int' +class KeyStoreKeys(str, Enum): + BOT_START_TIME = 'bot_start_time' + STARTUP_TIME = 'startup_time' + + class _KeyValueStoreModel(ModelBase): """ Pair Locks database model. @@ -27,9 +32,9 @@ class _KeyValueStoreModel(ModelBase): id: Mapped[int] = mapped_column(primary_key=True) - key: Mapped[str] = mapped_column(String(25), nullable=False, index=True) + key: Mapped[KeyStoreKeys] = mapped_column(String(25), nullable=False, index=True) - value_type: Mapped[ValueTypesEnum] = mapped_column(String(25), nullable=False) + value_type: Mapped[ValueTypesEnum] = mapped_column(String(20), nullable=False) string_value: Mapped[Optional[str]] datetime_value: Mapped[Optional[datetime]] @@ -45,7 +50,7 @@ class KeyValueStore(): """ @staticmethod - def store_value(key: str, value: ValueTypes) -> None: + def store_value(key: KeyStoreKeys, value: ValueTypes) -> None: """ Store the given value for the given key. :param key: Key to store the value for - can be used in get-value to retrieve the key @@ -73,7 +78,7 @@ class KeyValueStore(): _KeyValueStoreModel.session.commit() @staticmethod - def delete_value(key: str) -> None: + def delete_value(key: KeyStoreKeys) -> None: """ Delete the value for the given key. :param key: Key to delete the value for @@ -85,7 +90,7 @@ class KeyValueStore(): _KeyValueStoreModel.session.commit() @staticmethod - def get_value(key: str) -> Optional[ValueTypes]: + def get_value(key: KeyStoreKeys) -> Optional[ValueTypes]: """ Get the value for the given key. :param key: Key to get the value for @@ -106,7 +111,7 @@ class KeyValueStore(): raise ValueError(f'Unknown value type {kv.value_type}') # pragma: no cover @staticmethod - def get_string_value(key: str) -> Optional[str]: + def get_string_value(key: KeyStoreKeys) -> Optional[str]: """ Get the value for the given key. :param key: Key to get the value for @@ -119,7 +124,7 @@ class KeyValueStore(): return kv.string_value @staticmethod - def get_datetime_value(key: str) -> Optional[datetime]: + def get_datetime_value(key: KeyStoreKeys) -> Optional[datetime]: """ Get the value for the given key. :param key: Key to get the value for @@ -132,7 +137,7 @@ class KeyValueStore(): return kv.datetime_value.replace(tzinfo=timezone.utc) @staticmethod - def get_float_value(key: str) -> Optional[float]: + def get_float_value(key: KeyStoreKeys) -> Optional[float]: """ Get the value for the given key. :param key: Key to get the value for @@ -145,7 +150,7 @@ class KeyValueStore(): return kv.float_value @staticmethod - def get_int_value(key: str) -> Optional[int]: + def get_int_value(key: KeyStoreKeys) -> Optional[int]: """ Get the value for the given key. :param key: Key to get the value for From cf2cb94f8d28293373b856bcea71d6fc4229b329 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 8 Apr 2023 16:38:44 +0200 Subject: [PATCH 07/10] Add bot start date to `/profit` output --- freqtrade/rpc/rpc.py | 6 +++++- freqtrade/rpc/telegram.py | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 2b5eb107c..dc1c4c080 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -26,7 +26,8 @@ from freqtrade.exceptions import ExchangeError, PricingError from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs from freqtrade.loggers import bufferHandler from freqtrade.misc import decimals_per_coin, shorten_date -from freqtrade.persistence import Order, PairLocks, Trade +from freqtrade.persistence import KeyValueStore, Order, PairLocks, Trade +from freqtrade.persistence.key_value_store import KeyStoreKeys from freqtrade.persistence.models import PairLock from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist from freqtrade.rpc.fiat_convert import CryptoToFiatConverter @@ -543,6 +544,7 @@ class RPC: first_date = trades[0].open_date if trades else None last_date = trades[-1].open_date if trades else None num = float(len(durations) or 1) + bot_start = KeyValueStore.get_datetime_value(KeyStoreKeys.BOT_START_TIME) return { 'profit_closed_coin': profit_closed_coin_sum, 'profit_closed_percent_mean': round(profit_closed_ratio_mean * 100, 2), @@ -576,6 +578,8 @@ class RPC: 'max_drawdown': max_drawdown, 'max_drawdown_abs': max_drawdown_abs, 'trading_volume': trading_volume, + 'bot_start_timestamp': int(bot_start.timestamp() * 1000) if bot_start else 0, + 'bot_start_date': bot_start.strftime(DATETIME_PRINT_FORMAT) if bot_start else '', } def _rpc_balance(self, stake_currency: str, fiat_display_currency: str) -> Dict: diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index d79d8ea76..8637052de 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -819,7 +819,7 @@ class Telegram(RPCHandler): best_pair = stats['best_pair'] best_pair_profit_ratio = stats['best_pair_profit_ratio'] if stats['trade_count'] == 0: - markdown_msg = 'No trades yet.' + markdown_msg = f"No trades yet.\n*Bot started:* `{stats['bot_start_date']}`" else: # Message to display if stats['closed_trade_count'] > 0: @@ -838,6 +838,7 @@ class Telegram(RPCHandler): f"({profit_all_percent} \N{GREEK CAPITAL LETTER SIGMA}%)`\n" f"∙ `{round_coin_value(profit_all_fiat, fiat_disp_cur)}`\n" f"*Total Trade Count:* `{trade_count}`\n" + f"*Bot started:* `{stats['bot_start_date']}`\n" f"*{'First Trade opened' if not timescale else 'Showing Profit since'}:* " f"`{first_trade_date}`\n" f"*Latest Trade opened:* `{latest_trade_date}`\n" From be72670ca2ba979067b7486a0224d2f9c395e14a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 8 Apr 2023 16:40:14 +0200 Subject: [PATCH 08/10] Add documentation about /profit change --- docs/telegram-usage.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index dc0ab0976..fe990790a 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -279,6 +279,7 @@ Return a summary of your profit/loss and performance. > ∙ `33.095 EUR` > > **Total Trade Count:** `138` +> **Bot started:** `2022-07-11 18:40:44` > **First Trade opened:** `3 days ago` > **Latest Trade opened:** `2 minutes ago` > **Avg. Duration:** `2:33:45` @@ -292,6 +293,7 @@ The relative profit of `15.2 Σ%` is be based on the starting capital - so in th Starting capital is either taken from the `available_capital` setting, or calculated by using current wallet size - profits. Profit Factor is calculated as gross profits / gross losses - and should serve as an overall metric for the strategy. Max drawdown corresponds to the backtesting metric `Absolute Drawdown (Account)` - calculated as `(Absolute Drawdown) / (DrawdownHigh + startingBalance)`. +Bot started date will refer to the date the bot was first started. For older bots, this will default to the first trade's open date. ### /forceexit From a102cfdfc905a8bb7d76042a390198f7e6ca1d1a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 8 Apr 2023 16:40:22 +0200 Subject: [PATCH 09/10] Add new /profit fields to API --- freqtrade/freqtradebot.py | 2 +- freqtrade/rpc/api_server/api_schemas.py | 2 ++ tests/rpc/test_rpc_apiserver.py | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index c4ef7794f..73b25a7a1 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -26,7 +26,7 @@ from freqtrade.exchange import (ROUND_DOWN, ROUND_UP, timeframe_to_minutes, time from freqtrade.misc import safe_value_fallback, safe_value_fallback2 from freqtrade.mixins import LoggingMixin from freqtrade.persistence import Order, PairLocks, Trade, init_db -from freqtrade.persistence.key_value_store import KeyValueStore, set_startup_time +from freqtrade.persistence.key_value_store import set_startup_time from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.plugins.protectionmanager import ProtectionManager from freqtrade.resolvers import ExchangeResolver, StrategyResolver diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 7497b27f1..53bf7558f 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -108,6 +108,8 @@ class Profit(BaseModel): max_drawdown: float max_drawdown_abs: float trading_volume: Optional[float] + bot_start_timestamp: int + bot_start_date: str class SellReason(BaseModel): diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 31075e514..21994d4cd 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -883,6 +883,8 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, is_short, expected) 'max_drawdown': ANY, 'max_drawdown_abs': ANY, 'trading_volume': expected['trading_volume'], + 'bot_start_timestamp': 0, + 'bot_start_date': '', } From f5a5c2d6b9b6c14ff878808f7e757272d0fc9d64 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 8 Apr 2023 16:44:33 +0200 Subject: [PATCH 10/10] Improve imports --- freqtrade/persistence/__init__.py | 2 +- freqtrade/rpc/rpc.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/freqtrade/persistence/__init__.py b/freqtrade/persistence/__init__.py index c69a54b7f..4cf7aa455 100644 --- a/freqtrade/persistence/__init__.py +++ b/freqtrade/persistence/__init__.py @@ -1,6 +1,6 @@ # flake8: noqa: F401 -from freqtrade.persistence.key_value_store import KeyValueStore +from freqtrade.persistence.key_value_store import KeyStoreKeys, KeyValueStore from freqtrade.persistence.models import init_db from freqtrade.persistence.pairlock_middleware import PairLocks from freqtrade.persistence.trade_model import LocalTrade, Order, Trade diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index dc1c4c080..05ea848f7 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -26,8 +26,7 @@ from freqtrade.exceptions import ExchangeError, PricingError from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs from freqtrade.loggers import bufferHandler from freqtrade.misc import decimals_per_coin, shorten_date -from freqtrade.persistence import KeyValueStore, Order, PairLocks, Trade -from freqtrade.persistence.key_value_store import KeyStoreKeys +from freqtrade.persistence import KeyStoreKeys, KeyValueStore, Order, PairLocks, Trade from freqtrade.persistence.models import PairLock from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist from freqtrade.rpc.fiat_convert import CryptoToFiatConverter