Merge pull request #8459 from freqtrade/feat/kvstore
Add initial bot start time to /profit endpoint
This commit is contained in:
commit
605cc20a21
@ -279,6 +279,7 @@ Return a summary of your profit/loss and performance.
|
|||||||
> ∙ `33.095 EUR`
|
> ∙ `33.095 EUR`
|
||||||
>
|
>
|
||||||
> **Total Trade Count:** `138`
|
> **Total Trade Count:** `138`
|
||||||
|
> **Bot started:** `2022-07-11 18:40:44`
|
||||||
> **First Trade opened:** `3 days ago`
|
> **First Trade opened:** `3 days ago`
|
||||||
> **Latest Trade opened:** `2 minutes ago`
|
> **Latest Trade opened:** `2 minutes ago`
|
||||||
> **Avg. Duration:** `2:33:45`
|
> **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.
|
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.
|
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)`.
|
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 <trade_id>
|
### /forceexit <trade_id>
|
||||||
|
|
||||||
|
@ -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.misc import safe_value_fallback, safe_value_fallback2
|
||||||
from freqtrade.mixins import LoggingMixin
|
from freqtrade.mixins import LoggingMixin
|
||||||
from freqtrade.persistence import Order, PairLocks, Trade, init_db
|
from freqtrade.persistence import Order, PairLocks, Trade, init_db
|
||||||
|
from freqtrade.persistence.key_value_store import set_startup_time
|
||||||
from freqtrade.plugins.pairlistmanager import PairListManager
|
from freqtrade.plugins.pairlistmanager import PairListManager
|
||||||
from freqtrade.plugins.protectionmanager import ProtectionManager
|
from freqtrade.plugins.protectionmanager import ProtectionManager
|
||||||
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
||||||
@ -182,6 +183,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
performs startup tasks
|
performs startup tasks
|
||||||
"""
|
"""
|
||||||
migrate_binance_futures_names(self.config)
|
migrate_binance_futures_names(self.config)
|
||||||
|
set_startup_time()
|
||||||
|
|
||||||
self.rpc.startup_messages(self.config, self.pairlists, self.protections)
|
self.rpc.startup_messages(self.config, self.pairlists, self.protections)
|
||||||
# Update older trades with precision and precision mode
|
# Update older trades with precision and precision mode
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
# flake8: noqa: F401
|
# flake8: noqa: F401
|
||||||
|
|
||||||
|
from freqtrade.persistence.key_value_store import KeyStoreKeys, KeyValueStore
|
||||||
from freqtrade.persistence.models import init_db
|
from freqtrade.persistence.models import init_db
|
||||||
from freqtrade.persistence.pairlock_middleware import PairLocks
|
from freqtrade.persistence.pairlock_middleware import PairLocks
|
||||||
from freqtrade.persistence.trade_model import LocalTrade, Order, Trade
|
from freqtrade.persistence.trade_model import LocalTrade, Order, Trade
|
||||||
|
179
freqtrade/persistence/key_value_store.py
Normal file
179
freqtrade/persistence/key_value_store.py
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
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 KeyStoreKeys(str, Enum):
|
||||||
|
BOT_START_TIME = 'bot_start_time'
|
||||||
|
STARTUP_TIME = 'startup_time'
|
||||||
|
|
||||||
|
|
||||||
|
class _KeyValueStoreModel(ModelBase):
|
||||||
|
"""
|
||||||
|
Pair Locks database model.
|
||||||
|
"""
|
||||||
|
__tablename__ = 'KeyValueStore'
|
||||||
|
session: ClassVar[SessionType]
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
|
||||||
|
key: Mapped[KeyStoreKeys] = mapped_column(String(25), nullable=False, index=True)
|
||||||
|
|
||||||
|
value_type: Mapped[ValueTypesEnum] = mapped_column(String(20), nullable=False)
|
||||||
|
|
||||||
|
string_value: Mapped[Optional[str]]
|
||||||
|
datetime_value: Mapped[Optional[datetime]]
|
||||||
|
float_value: Mapped[Optional[float]]
|
||||||
|
int_value: Mapped[Optional[int]]
|
||||||
|
|
||||||
|
|
||||||
|
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 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
|
||||||
|
:param value: Value to store - can be str, datetime, float or int
|
||||||
|
"""
|
||||||
|
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: KeyStoreKeys) -> 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()
|
||||||
|
if kv is not None:
|
||||||
|
_KeyValueStoreModel.session.delete(kv)
|
||||||
|
_KeyValueStoreModel.session.commit()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_value(key: KeyStoreKeys) -> 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: KeyStoreKeys) -> 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: KeyStoreKeys) -> 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: KeyStoreKeys) -> 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: KeyStoreKeys) -> 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():
|
||||||
|
"""
|
||||||
|
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))
|
@ -13,6 +13,7 @@ from sqlalchemy.pool import StaticPool
|
|||||||
|
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.persistence.base import ModelBase
|
from freqtrade.persistence.base import ModelBase
|
||||||
|
from freqtrade.persistence.key_value_store import _KeyValueStoreModel
|
||||||
from freqtrade.persistence.migrations import check_migrate
|
from freqtrade.persistence.migrations import check_migrate
|
||||||
from freqtrade.persistence.pairlock import PairLock
|
from freqtrade.persistence.pairlock import PairLock
|
||||||
from freqtrade.persistence.trade_model import Order, Trade
|
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)
|
bind=engine, autoflush=False), scopefunc=get_request_or_thread_id)
|
||||||
Order.session = Trade.session
|
Order.session = Trade.session
|
||||||
PairLock.session = Trade.session
|
PairLock.session = Trade.session
|
||||||
|
_KeyValueStoreModel.session = Trade.session
|
||||||
|
|
||||||
previous_tables = inspect(engine).get_table_names()
|
previous_tables = inspect(engine).get_table_names()
|
||||||
ModelBase.metadata.create_all(engine)
|
ModelBase.metadata.create_all(engine)
|
||||||
|
@ -108,6 +108,8 @@ class Profit(BaseModel):
|
|||||||
max_drawdown: float
|
max_drawdown: float
|
||||||
max_drawdown_abs: float
|
max_drawdown_abs: float
|
||||||
trading_volume: Optional[float]
|
trading_volume: Optional[float]
|
||||||
|
bot_start_timestamp: int
|
||||||
|
bot_start_date: str
|
||||||
|
|
||||||
|
|
||||||
class SellReason(BaseModel):
|
class SellReason(BaseModel):
|
||||||
|
@ -26,7 +26,7 @@ from freqtrade.exceptions import ExchangeError, PricingError
|
|||||||
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs
|
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs
|
||||||
from freqtrade.loggers import bufferHandler
|
from freqtrade.loggers import bufferHandler
|
||||||
from freqtrade.misc import decimals_per_coin, shorten_date
|
from freqtrade.misc import decimals_per_coin, shorten_date
|
||||||
from freqtrade.persistence import Order, PairLocks, Trade
|
from freqtrade.persistence import KeyStoreKeys, KeyValueStore, Order, PairLocks, Trade
|
||||||
from freqtrade.persistence.models import PairLock
|
from freqtrade.persistence.models import PairLock
|
||||||
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
||||||
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
|
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
|
||||||
@ -543,6 +543,7 @@ class RPC:
|
|||||||
first_date = trades[0].open_date if trades else None
|
first_date = trades[0].open_date if trades else None
|
||||||
last_date = trades[-1].open_date if trades else None
|
last_date = trades[-1].open_date if trades else None
|
||||||
num = float(len(durations) or 1)
|
num = float(len(durations) or 1)
|
||||||
|
bot_start = KeyValueStore.get_datetime_value(KeyStoreKeys.BOT_START_TIME)
|
||||||
return {
|
return {
|
||||||
'profit_closed_coin': profit_closed_coin_sum,
|
'profit_closed_coin': profit_closed_coin_sum,
|
||||||
'profit_closed_percent_mean': round(profit_closed_ratio_mean * 100, 2),
|
'profit_closed_percent_mean': round(profit_closed_ratio_mean * 100, 2),
|
||||||
@ -576,6 +577,8 @@ class RPC:
|
|||||||
'max_drawdown': max_drawdown,
|
'max_drawdown': max_drawdown,
|
||||||
'max_drawdown_abs': max_drawdown_abs,
|
'max_drawdown_abs': max_drawdown_abs,
|
||||||
'trading_volume': trading_volume,
|
'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:
|
def _rpc_balance(self, stake_currency: str, fiat_display_currency: str) -> Dict:
|
||||||
|
@ -819,7 +819,7 @@ class Telegram(RPCHandler):
|
|||||||
best_pair = stats['best_pair']
|
best_pair = stats['best_pair']
|
||||||
best_pair_profit_ratio = stats['best_pair_profit_ratio']
|
best_pair_profit_ratio = stats['best_pair_profit_ratio']
|
||||||
if stats['trade_count'] == 0:
|
if stats['trade_count'] == 0:
|
||||||
markdown_msg = 'No trades yet.'
|
markdown_msg = f"No trades yet.\n*Bot started:* `{stats['bot_start_date']}`"
|
||||||
else:
|
else:
|
||||||
# Message to display
|
# Message to display
|
||||||
if stats['closed_trade_count'] > 0:
|
if stats['closed_trade_count'] > 0:
|
||||||
@ -838,6 +838,7 @@ class Telegram(RPCHandler):
|
|||||||
f"({profit_all_percent} \N{GREEK CAPITAL LETTER SIGMA}%)`\n"
|
f"({profit_all_percent} \N{GREEK CAPITAL LETTER SIGMA}%)`\n"
|
||||||
f"∙ `{round_coin_value(profit_all_fiat, fiat_disp_cur)}`\n"
|
f"∙ `{round_coin_value(profit_all_fiat, fiat_disp_cur)}`\n"
|
||||||
f"*Total Trade Count:* `{trade_count}`\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 opened' if not timescale else 'Showing Profit since'}:* "
|
||||||
f"`{first_trade_date}`\n"
|
f"`{first_trade_date}`\n"
|
||||||
f"*Latest Trade opened:* `{latest_trade_date}`\n"
|
f"*Latest Trade opened:* `{latest_trade_date}`\n"
|
||||||
|
69
tests/persistence/test_key_value_store.py
Normal file
69
tests/persistence/test_key_value_store.py
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from freqtrade.persistence.key_value_store import KeyValueStore, set_startup_time
|
||||||
|
from tests.conftest import create_mock_trades_usdt
|
||||||
|
|
||||||
|
|
||||||
|
@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") == "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)
|
||||||
|
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
|
||||||
|
# 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'})
|
||||||
|
|
||||||
|
|
||||||
|
@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
|
@ -883,6 +883,8 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, is_short, expected)
|
|||||||
'max_drawdown': ANY,
|
'max_drawdown': ANY,
|
||||||
'max_drawdown_abs': ANY,
|
'max_drawdown_abs': ANY,
|
||||||
'trading_volume': expected['trading_volume'],
|
'trading_volume': expected['trading_volume'],
|
||||||
|
'bot_start_timestamp': 0,
|
||||||
|
'bot_start_date': '',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user