mirror of
https://github.com/freqtrade/freqtrade.git
synced 2026-01-26 00:40:23 +00:00
Merge pull request #11158 from freqtrade/feat/plot_annotations
add support for plot_annotations
This commit is contained in:
BIN
docs/assets/freqUI-chart-annotations-dark.png
Normal file
BIN
docs/assets/freqUI-chart-annotations-dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 111 KiB |
BIN
docs/assets/freqUI-chart-annotations-light.png
Normal file
BIN
docs/assets/freqUI-chart-annotations-light.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 109 KiB |
@@ -1107,3 +1107,119 @@ class AwesomeStrategy(IStrategy):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Plot annotations callback
|
||||||
|
|
||||||
|
The plot annotations callback is called whenever freqUI requests data to display a chart.
|
||||||
|
This callback has no meaning in the trade cycle context and is only used for charting purposes.
|
||||||
|
|
||||||
|
The strategy can then return a list of `AnnotationType` objects to be displayed on the chart.
|
||||||
|
Depending on the content returned - the chart can display horizontal areas, vertical areas, or boxes.
|
||||||
|
|
||||||
|
The full object looks like this:
|
||||||
|
|
||||||
|
``` json
|
||||||
|
{
|
||||||
|
"type": "area", // Type of the annotation, currently only "area" is supported
|
||||||
|
"start": "2024-01-01 15:00:00", // Start date of the area
|
||||||
|
"end": "2024-01-01 16:00:00", // End date of the area
|
||||||
|
"y_start": 94000.2, // Price / y axis value
|
||||||
|
"y_end": 98000, // Price / y axis value
|
||||||
|
"color": "",
|
||||||
|
"label": "some label"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The below example will mark the chart with areas for the hours 8 and 15, with a grey color, highlighting the market open and close hours.
|
||||||
|
This is obviously a very basic example.
|
||||||
|
|
||||||
|
``` python
|
||||||
|
# Default imports
|
||||||
|
|
||||||
|
class AwesomeStrategy(IStrategy):
|
||||||
|
def plot_annotations(
|
||||||
|
self, pair: str, start_date: datetime, end_date: datetime, dataframe: DataFrame, **kwargs
|
||||||
|
) -> list[AnnotationType]:
|
||||||
|
"""
|
||||||
|
Retrieve area annotations for a chart.
|
||||||
|
Must be returned as array, with type, label, color, start, end, y_start, y_end.
|
||||||
|
All settings except for type are optional - though it usually makes sense to include either
|
||||||
|
"start and end" or "y_start and y_end" for either horizontal or vertical plots
|
||||||
|
(or all 4 for boxes).
|
||||||
|
:param pair: Pair that's currently analyzed
|
||||||
|
:param start_date: Start date of the chart data being requested
|
||||||
|
:param end_date: End date of the chart data being requested
|
||||||
|
:param dataframe: DataFrame with the analyzed data for the chart
|
||||||
|
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||||
|
:return: List of AnnotationType objects
|
||||||
|
"""
|
||||||
|
annotations = []
|
||||||
|
while start_dt < end_date:
|
||||||
|
start_dt += timedelta(hours=1)
|
||||||
|
if start_dt.hour in (8, 15):
|
||||||
|
annotations.append(
|
||||||
|
{
|
||||||
|
"type": "area",
|
||||||
|
"label": "Trade open and close hours",
|
||||||
|
"start": start_dt,
|
||||||
|
"end": start_dt + timedelta(hours=1),
|
||||||
|
# Omitting y_start and y_end will result in a vertical area spanning the whole height of the main Chart
|
||||||
|
"color": "rgba(133, 133, 133, 0.4)",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return annotations
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Entries will be validated, and won't be passed to the UI if they don't correspond to the expected schema and will log an error if they don't.
|
||||||
|
|
||||||
|
!!! Warning "Many annotations"
|
||||||
|
Using too many annotations can cause the UI to hang, especially when plotting large amounts of historic data.
|
||||||
|
Use the annotation feature with care.
|
||||||
|
|
||||||
|
### Plot annotations example
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
??? Info "Code used for the plot above"
|
||||||
|
This is an example code and should be treated as such.
|
||||||
|
|
||||||
|
``` python
|
||||||
|
# Default imports
|
||||||
|
|
||||||
|
class AwesomeStrategy(IStrategy):
|
||||||
|
def plot_annotations(
|
||||||
|
self, pair: str, start_date: datetime, end_date: datetime, dataframe: DataFrame, **kwargs
|
||||||
|
) -> list[AnnotationType]:
|
||||||
|
annotations = []
|
||||||
|
while start_dt < end_date:
|
||||||
|
start_dt += timedelta(hours=1)
|
||||||
|
if (start_dt.hour % 4) == 0:
|
||||||
|
mark_areas.append(
|
||||||
|
{
|
||||||
|
"type": "area",
|
||||||
|
"label": "4h",
|
||||||
|
"start": start_dt,
|
||||||
|
"end": start_dt + timedelta(hours=1),
|
||||||
|
"color": "rgba(133, 133, 133, 0.4)",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
elif (start_dt.hour % 2) == 0:
|
||||||
|
price = dataframe.loc[dataframe["date"] == start_dt, ["close"]].mean()
|
||||||
|
mark_areas.append(
|
||||||
|
{
|
||||||
|
"type": "area",
|
||||||
|
"label": "2h",
|
||||||
|
"start": start_dt,
|
||||||
|
"end": start_dt + timedelta(hours=1),
|
||||||
|
"y_end": price * 1.01,
|
||||||
|
"y_start": price * 0.99,
|
||||||
|
"color": "rgba(0, 255, 0, 0.4)",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return annotations
|
||||||
|
|
||||||
|
```
|
||||||
|
|||||||
@@ -7,4 +7,5 @@ from freqtrade.ft_types.backtest_result_type import (
|
|||||||
BacktestResultType,
|
BacktestResultType,
|
||||||
get_BacktestResultType_default,
|
get_BacktestResultType_default,
|
||||||
)
|
)
|
||||||
|
from freqtrade.ft_types.plot_annotation_type import AnnotationType
|
||||||
from freqtrade.ft_types.valid_exchanges_type import ValidExchangesType
|
from freqtrade.ft_types.valid_exchanges_type import ValidExchangesType
|
||||||
|
|||||||
18
freqtrade/ft_types/plot_annotation_type.py
Normal file
18
freqtrade/ft_types/plot_annotation_type.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
from pydantic import TypeAdapter
|
||||||
|
from typing_extensions import Required, TypedDict
|
||||||
|
|
||||||
|
|
||||||
|
class AnnotationType(TypedDict, total=False):
|
||||||
|
type: Required[Literal["area"]]
|
||||||
|
start: str | datetime
|
||||||
|
end: str | datetime
|
||||||
|
y_start: float
|
||||||
|
y_end: float
|
||||||
|
color: str
|
||||||
|
label: str
|
||||||
|
|
||||||
|
|
||||||
|
AnnotationTypeTA = TypeAdapter(AnnotationType)
|
||||||
@@ -5,7 +5,7 @@ from pydantic import AwareDatetime, BaseModel, RootModel, SerializeAsAny, model_
|
|||||||
|
|
||||||
from freqtrade.constants import DL_DATA_TIMEFRAMES, IntOrInf
|
from freqtrade.constants import DL_DATA_TIMEFRAMES, IntOrInf
|
||||||
from freqtrade.enums import MarginMode, OrderTypeValues, SignalDirection, TradingMode
|
from freqtrade.enums import MarginMode, OrderTypeValues, SignalDirection, TradingMode
|
||||||
from freqtrade.ft_types import ValidExchangesType
|
from freqtrade.ft_types import AnnotationType, ValidExchangesType
|
||||||
from freqtrade.rpc.api_server.webserver_bgwork import ProgressTask
|
from freqtrade.rpc.api_server.webserver_bgwork import ProgressTask
|
||||||
|
|
||||||
|
|
||||||
@@ -539,6 +539,7 @@ class PairHistory(BaseModel):
|
|||||||
columns: list[str]
|
columns: list[str]
|
||||||
all_columns: list[str] = []
|
all_columns: list[str] = []
|
||||||
data: SerializeAsAny[list[Any]]
|
data: SerializeAsAny[list[Any]]
|
||||||
|
annotations: list[AnnotationType] | None = None
|
||||||
length: int
|
length: int
|
||||||
buy_signals: int
|
buy_signals: int
|
||||||
sell_signals: int
|
sell_signals: int
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ from freqtrade.enums import (
|
|||||||
from freqtrade.exceptions import ExchangeError, PricingError
|
from freqtrade.exceptions import ExchangeError, PricingError
|
||||||
from freqtrade.exchange import Exchange, timeframe_to_minutes, timeframe_to_msecs
|
from freqtrade.exchange import Exchange, timeframe_to_minutes, timeframe_to_msecs
|
||||||
from freqtrade.exchange.exchange_utils import price_to_precision
|
from freqtrade.exchange.exchange_utils import price_to_precision
|
||||||
|
from freqtrade.ft_types import AnnotationType
|
||||||
from freqtrade.loggers import bufferHandler
|
from freqtrade.loggers import bufferHandler
|
||||||
from freqtrade.persistence import CustomDataWrapper, KeyValueStore, PairLocks, Trade
|
from freqtrade.persistence import CustomDataWrapper, KeyValueStore, PairLocks, Trade
|
||||||
from freqtrade.persistence.models import PairLock
|
from freqtrade.persistence.models import PairLock
|
||||||
@@ -1356,6 +1357,7 @@ class RPC:
|
|||||||
dataframe: DataFrame,
|
dataframe: DataFrame,
|
||||||
last_analyzed: datetime,
|
last_analyzed: datetime,
|
||||||
selected_cols: list[str] | None,
|
selected_cols: list[str] | None,
|
||||||
|
annotations: list[AnnotationType],
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
has_content = len(dataframe) != 0
|
has_content = len(dataframe) != 0
|
||||||
dataframe_columns = list(dataframe.columns)
|
dataframe_columns = list(dataframe.columns)
|
||||||
@@ -1411,6 +1413,7 @@ class RPC:
|
|||||||
"data_start_ts": 0,
|
"data_start_ts": 0,
|
||||||
"data_stop": "",
|
"data_stop": "",
|
||||||
"data_stop_ts": 0,
|
"data_stop_ts": 0,
|
||||||
|
"annotations": annotations,
|
||||||
}
|
}
|
||||||
if has_content:
|
if has_content:
|
||||||
res.update(
|
res.update(
|
||||||
@@ -1429,8 +1432,16 @@ class RPC:
|
|||||||
"""Analyzed dataframe in Dict form"""
|
"""Analyzed dataframe in Dict form"""
|
||||||
|
|
||||||
_data, last_analyzed = self.__rpc_analysed_dataframe_raw(pair, timeframe, limit)
|
_data, last_analyzed = self.__rpc_analysed_dataframe_raw(pair, timeframe, limit)
|
||||||
|
annotations = self._freqtrade.strategy.ft_plot_annotations(pair=pair, dataframe=_data)
|
||||||
|
|
||||||
return RPC._convert_dataframe_to_dict(
|
return RPC._convert_dataframe_to_dict(
|
||||||
self._freqtrade.config["strategy"], pair, timeframe, _data, last_analyzed, selected_cols
|
self._freqtrade.config["strategy"],
|
||||||
|
pair,
|
||||||
|
timeframe,
|
||||||
|
_data,
|
||||||
|
last_analyzed,
|
||||||
|
selected_cols,
|
||||||
|
annotations,
|
||||||
)
|
)
|
||||||
|
|
||||||
def __rpc_analysed_dataframe_raw(
|
def __rpc_analysed_dataframe_raw(
|
||||||
@@ -1531,6 +1542,7 @@ class RPC:
|
|||||||
)
|
)
|
||||||
data = _data[pair]
|
data = _data[pair]
|
||||||
|
|
||||||
|
annotations = []
|
||||||
if config.get("strategy"):
|
if config.get("strategy"):
|
||||||
strategy.dp = DataProvider(config, exchange=exchange, pairlists=None)
|
strategy.dp = DataProvider(config, exchange=exchange, pairlists=None)
|
||||||
strategy.ft_bot_start()
|
strategy.ft_bot_start()
|
||||||
@@ -1539,6 +1551,8 @@ class RPC:
|
|||||||
df_analyzed = trim_dataframe(
|
df_analyzed = trim_dataframe(
|
||||||
df_analyzed, timerange_parsed, startup_candles=startup_candles
|
df_analyzed, timerange_parsed, startup_candles=startup_candles
|
||||||
)
|
)
|
||||||
|
annotations = strategy.ft_plot_annotations(pair=pair, dataframe=df_analyzed)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
df_analyzed = data
|
df_analyzed = data
|
||||||
|
|
||||||
@@ -1549,6 +1563,7 @@ class RPC:
|
|||||||
df_analyzed.copy(),
|
df_analyzed.copy(),
|
||||||
dt_now(),
|
dt_now(),
|
||||||
selected_cols,
|
selected_cols,
|
||||||
|
annotations,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _rpc_plot_config(self) -> dict[str, Any]:
|
def _rpc_plot_config(self) -> dict[str, Any]:
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from freqtrade.exchange import (
|
|||||||
timeframe_to_prev_date,
|
timeframe_to_prev_date,
|
||||||
timeframe_to_seconds,
|
timeframe_to_seconds,
|
||||||
)
|
)
|
||||||
|
from freqtrade.ft_types import AnnotationType
|
||||||
from freqtrade.persistence import Order, PairLocks, Trade
|
from freqtrade.persistence import Order, PairLocks, Trade
|
||||||
from freqtrade.strategy.informative_decorator import informative
|
from freqtrade.strategy.informative_decorator import informative
|
||||||
from freqtrade.strategy.interface import IStrategy
|
from freqtrade.strategy.interface import IStrategy
|
||||||
@@ -44,4 +45,5 @@ __all__ = [
|
|||||||
"merge_informative_pair",
|
"merge_informative_pair",
|
||||||
"stoploss_from_absolute",
|
"stoploss_from_absolute",
|
||||||
"stoploss_from_open",
|
"stoploss_from_open",
|
||||||
|
"AnnotationType",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from datetime import datetime, timedelta, timezone
|
|||||||
from math import isinf, isnan
|
from math import isinf, isnan
|
||||||
|
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
from pydantic import ValidationError
|
||||||
|
|
||||||
from freqtrade.constants import CUSTOM_TAG_MAX_LENGTH, Config, IntOrInf, ListPairsWithTimeframes
|
from freqtrade.constants import CUSTOM_TAG_MAX_LENGTH, Config, IntOrInf, ListPairsWithTimeframes
|
||||||
from freqtrade.data.converter import populate_dataframe_with_trades
|
from freqtrade.data.converter import populate_dataframe_with_trades
|
||||||
@@ -27,6 +28,7 @@ from freqtrade.enums import (
|
|||||||
)
|
)
|
||||||
from freqtrade.exceptions import OperationalException, StrategyError
|
from freqtrade.exceptions import OperationalException, StrategyError
|
||||||
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date, timeframe_to_seconds
|
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date, timeframe_to_seconds
|
||||||
|
from freqtrade.ft_types import AnnotationType
|
||||||
from freqtrade.misc import remove_entry_exit_signals
|
from freqtrade.misc import remove_entry_exit_signals
|
||||||
from freqtrade.persistence import Order, PairLocks, Trade
|
from freqtrade.persistence import Order, PairLocks, Trade
|
||||||
from freqtrade.strategy.hyper import HyperStrategyMixin
|
from freqtrade.strategy.hyper import HyperStrategyMixin
|
||||||
@@ -834,6 +836,24 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
"""
|
"""
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def plot_annotations(
|
||||||
|
self, pair: str, start_date: datetime, end_date: datetime, dataframe: DataFrame, **kwargs
|
||||||
|
) -> list[AnnotationType]:
|
||||||
|
"""
|
||||||
|
Retrieve area annotations for a chart.
|
||||||
|
Must be returned as array, with type, label, color, start, end, y_start, y_end.
|
||||||
|
All settings except for type are optional - though it usually makes sense to include either
|
||||||
|
"start and end" or "y_start and y_end" for either horizontal or vertical plots
|
||||||
|
(or all 4 for boxes).
|
||||||
|
:param pair: Pair that's currently analyzed
|
||||||
|
:param start_date: Start date of the chart data being requested
|
||||||
|
:param end_date: End date of the chart data being requested
|
||||||
|
:param dataframe: DataFrame with the analyzed data for the chart
|
||||||
|
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||||
|
:return: List of AnnotationType objects
|
||||||
|
"""
|
||||||
|
return []
|
||||||
|
|
||||||
def populate_any_indicators(
|
def populate_any_indicators(
|
||||||
self,
|
self,
|
||||||
pair: str,
|
pair: str,
|
||||||
@@ -1780,3 +1800,33 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
if "exit_long" not in df.columns:
|
if "exit_long" not in df.columns:
|
||||||
df = df.rename({"sell": "exit_long"}, axis="columns")
|
df = df.rename({"sell": "exit_long"}, axis="columns")
|
||||||
return df
|
return df
|
||||||
|
|
||||||
|
def ft_plot_annotations(self, pair: str, dataframe: DataFrame) -> list[AnnotationType]:
|
||||||
|
"""
|
||||||
|
Internal wrapper around plot_dataframe
|
||||||
|
"""
|
||||||
|
if len(dataframe) > 0:
|
||||||
|
annotations = strategy_safe_wrapper(self.plot_annotations)(
|
||||||
|
pair=pair,
|
||||||
|
dataframe=dataframe,
|
||||||
|
start_date=dataframe.iloc[0]["date"].to_pydatetime(),
|
||||||
|
end_date=dataframe.iloc[-1]["date"].to_pydatetime(),
|
||||||
|
)
|
||||||
|
|
||||||
|
from freqtrade.ft_types.plot_annotation_type import AnnotationTypeTA
|
||||||
|
|
||||||
|
annotations_new: list[AnnotationType] = []
|
||||||
|
for annotation in annotations:
|
||||||
|
if isinstance(annotation, dict):
|
||||||
|
# Convert to AnnotationType
|
||||||
|
try:
|
||||||
|
AnnotationTypeTA.validate_python(annotation)
|
||||||
|
annotations_new.append(annotation)
|
||||||
|
except ValidationError as e:
|
||||||
|
logger.error(f"Invalid annotation data: {annotation}. Error: {e}")
|
||||||
|
else:
|
||||||
|
# Already an AnnotationType
|
||||||
|
annotations_new.append(annotation)
|
||||||
|
|
||||||
|
return annotations_new
|
||||||
|
return []
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ from freqtrade.strategy import (
|
|||||||
merge_informative_pair,
|
merge_informative_pair,
|
||||||
stoploss_from_absolute,
|
stoploss_from_absolute,
|
||||||
stoploss_from_open,
|
stoploss_from_open,
|
||||||
|
AnnotationType,
|
||||||
)
|
)
|
||||||
|
|
||||||
# --------------------------------
|
# --------------------------------
|
||||||
|
|||||||
@@ -399,3 +399,21 @@ def order_filled(
|
|||||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def plot_annotations(
|
||||||
|
self, pair: str, start_date: datetime, end_date: datetime, dataframe: DataFrame, **kwargs
|
||||||
|
) -> list[AnnotationType]:
|
||||||
|
"""
|
||||||
|
Retrieve area annotations for a chart.
|
||||||
|
Must be returned as array, with type, label, color, start, end, y_start, y_end.
|
||||||
|
All settings except for type are optional - though it usually makes sense to include either
|
||||||
|
"start and end" or "y_start and y_end" for either horizontal or vertical plots
|
||||||
|
(or all 4 for boxes).
|
||||||
|
:param pair: Pair that's currently analyzed
|
||||||
|
:param start_date: Start date of the chart data being requested
|
||||||
|
:param end_date: End date of the chart data being requested
|
||||||
|
:param dataframe: DataFrame with the analyzed data for the chart
|
||||||
|
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||||
|
:return: List of AnnotationType objects
|
||||||
|
"""
|
||||||
|
return []
|
||||||
|
|||||||
@@ -1864,7 +1864,21 @@ def test_api_pair_candles(botclient, ohlcv_history):
|
|||||||
ohlcv_history["exit_short"] = 0
|
ohlcv_history["exit_short"] = 0
|
||||||
|
|
||||||
ftbot.dataprovider._set_cached_df("XRP/BTC", timeframe, ohlcv_history, CandleType.SPOT)
|
ftbot.dataprovider._set_cached_df("XRP/BTC", timeframe, ohlcv_history, CandleType.SPOT)
|
||||||
|
fake_plot_annotations = [
|
||||||
|
{
|
||||||
|
"type": "area",
|
||||||
|
"start": "2024-01-01 15:00:00",
|
||||||
|
"end": "2024-01-01 16:00:00",
|
||||||
|
"y_start": 94000.2,
|
||||||
|
"y_end": 98000,
|
||||||
|
"color": "",
|
||||||
|
"label": "some label",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
plot_annotations_mock = MagicMock(return_value=fake_plot_annotations)
|
||||||
|
ftbot.strategy.plot_annotations = plot_annotations_mock
|
||||||
for call in ("get", "post"):
|
for call in ("get", "post"):
|
||||||
|
plot_annotations_mock.reset_mock()
|
||||||
if call == "get":
|
if call == "get":
|
||||||
rc = client_get(
|
rc = client_get(
|
||||||
client,
|
client,
|
||||||
@@ -1894,6 +1908,8 @@ def test_api_pair_candles(botclient, ohlcv_history):
|
|||||||
assert resp["data_start_ts"] == 1511686200000
|
assert resp["data_start_ts"] == 1511686200000
|
||||||
assert resp["data_stop"] == "2017-11-26 09:00:00+00:00"
|
assert resp["data_stop"] == "2017-11-26 09:00:00+00:00"
|
||||||
assert resp["data_stop_ts"] == 1511686800000
|
assert resp["data_stop_ts"] == 1511686800000
|
||||||
|
assert resp["annotations"] == fake_plot_annotations
|
||||||
|
assert plot_annotations_mock.call_count == 1
|
||||||
assert isinstance(resp["columns"], list)
|
assert isinstance(resp["columns"], list)
|
||||||
base_cols = {
|
base_cols = {
|
||||||
"date",
|
"date",
|
||||||
@@ -2235,6 +2251,7 @@ def test_api_pair_history(botclient, tmp_path, mocker):
|
|||||||
assert result["data_start_ts"] == 1515628800000
|
assert result["data_start_ts"] == 1515628800000
|
||||||
assert result["data_stop"] == "2018-01-12 00:00:00+00:00"
|
assert result["data_stop"] == "2018-01-12 00:00:00+00:00"
|
||||||
assert result["data_stop_ts"] == 1515715200000
|
assert result["data_stop_ts"] == 1515715200000
|
||||||
|
assert result["annotations"] == []
|
||||||
lfm.reset_mock()
|
lfm.reset_mock()
|
||||||
|
|
||||||
# No data found
|
# No data found
|
||||||
|
|||||||
Reference in New Issue
Block a user