Skip to content

Commit 689da4c

Browse files
authored
Merge pull request freqtrade#11158 from freqtrade/feat/plot_annotations
add support for plot_annotations
2 parents 9c4abcc + 87d954a commit 689da4c

File tree

12 files changed

+241
-2
lines changed

12 files changed

+241
-2
lines changed
111 KB
Loading
109 KB
Loading

docs/strategy-callbacks.md

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1107,3 +1107,119 @@ class AwesomeStrategy(IStrategy):
11071107
return None
11081108

11091109
```
1110+
1111+
## Plot annotations callback
1112+
1113+
The plot annotations callback is called whenever freqUI requests data to display a chart.
1114+
This callback has no meaning in the trade cycle context and is only used for charting purposes.
1115+
1116+
The strategy can then return a list of `AnnotationType` objects to be displayed on the chart.
1117+
Depending on the content returned - the chart can display horizontal areas, vertical areas, or boxes.
1118+
1119+
The full object looks like this:
1120+
1121+
``` json
1122+
{
1123+
"type": "area", // Type of the annotation, currently only "area" is supported
1124+
"start": "2024-01-01 15:00:00", // Start date of the area
1125+
"end": "2024-01-01 16:00:00", // End date of the area
1126+
"y_start": 94000.2, // Price / y axis value
1127+
"y_end": 98000, // Price / y axis value
1128+
"color": "",
1129+
"label": "some label"
1130+
}
1131+
```
1132+
1133+
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.
1134+
This is obviously a very basic example.
1135+
1136+
``` python
1137+
# Default imports
1138+
1139+
class AwesomeStrategy(IStrategy):
1140+
def plot_annotations(
1141+
self, pair: str, start_date: datetime, end_date: datetime, dataframe: DataFrame, **kwargs
1142+
) -> list[AnnotationType]:
1143+
"""
1144+
Retrieve area annotations for a chart.
1145+
Must be returned as array, with type, label, color, start, end, y_start, y_end.
1146+
All settings except for type are optional - though it usually makes sense to include either
1147+
"start and end" or "y_start and y_end" for either horizontal or vertical plots
1148+
(or all 4 for boxes).
1149+
:param pair: Pair that's currently analyzed
1150+
:param start_date: Start date of the chart data being requested
1151+
:param end_date: End date of the chart data being requested
1152+
:param dataframe: DataFrame with the analyzed data for the chart
1153+
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
1154+
:return: List of AnnotationType objects
1155+
"""
1156+
annotations = []
1157+
while start_dt < end_date:
1158+
start_dt += timedelta(hours=1)
1159+
if start_dt.hour in (8, 15):
1160+
annotations.append(
1161+
{
1162+
"type": "area",
1163+
"label": "Trade open and close hours",
1164+
"start": start_dt,
1165+
"end": start_dt + timedelta(hours=1),
1166+
# Omitting y_start and y_end will result in a vertical area spanning the whole height of the main Chart
1167+
"color": "rgba(133, 133, 133, 0.4)",
1168+
}
1169+
)
1170+
1171+
return annotations
1172+
1173+
```
1174+
1175+
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.
1176+
1177+
!!! Warning "Many annotations"
1178+
Using too many annotations can cause the UI to hang, especially when plotting large amounts of historic data.
1179+
Use the annotation feature with care.
1180+
1181+
### Plot annotations example
1182+
1183+
![FreqUI - plot Annotations](assets/freqUI-chart-annotations-dark.png#only-dark)
1184+
![FreqUI - plot Annotations](assets/freqUI-chart-annotations-light.png#only-light)
1185+
1186+
??? Info "Code used for the plot above"
1187+
This is an example code and should be treated as such.
1188+
1189+
``` python
1190+
# Default imports
1191+
1192+
class AwesomeStrategy(IStrategy):
1193+
def plot_annotations(
1194+
self, pair: str, start_date: datetime, end_date: datetime, dataframe: DataFrame, **kwargs
1195+
) -> list[AnnotationType]:
1196+
annotations = []
1197+
while start_dt < end_date:
1198+
start_dt += timedelta(hours=1)
1199+
if (start_dt.hour % 4) == 0:
1200+
mark_areas.append(
1201+
{
1202+
"type": "area",
1203+
"label": "4h",
1204+
"start": start_dt,
1205+
"end": start_dt + timedelta(hours=1),
1206+
"color": "rgba(133, 133, 133, 0.4)",
1207+
}
1208+
)
1209+
elif (start_dt.hour % 2) == 0:
1210+
price = dataframe.loc[dataframe["date"] == start_dt, ["close"]].mean()
1211+
mark_areas.append(
1212+
{
1213+
"type": "area",
1214+
"label": "2h",
1215+
"start": start_dt,
1216+
"end": start_dt + timedelta(hours=1),
1217+
"y_end": price * 1.01,
1218+
"y_start": price * 0.99,
1219+
"color": "rgba(0, 255, 0, 0.4)",
1220+
}
1221+
)
1222+
1223+
return annotations
1224+
1225+
```

freqtrade/ft_types/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@
77
BacktestResultType,
88
get_BacktestResultType_default,
99
)
10+
from freqtrade.ft_types.plot_annotation_type import AnnotationType
1011
from freqtrade.ft_types.valid_exchanges_type import ValidExchangesType
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from datetime import datetime
2+
from typing import Literal
3+
4+
from pydantic import TypeAdapter
5+
from typing_extensions import Required, TypedDict
6+
7+
8+
class AnnotationType(TypedDict, total=False):
9+
type: Required[Literal["area"]]
10+
start: str | datetime
11+
end: str | datetime
12+
y_start: float
13+
y_end: float
14+
color: str
15+
label: str
16+
17+
18+
AnnotationTypeTA = TypeAdapter(AnnotationType)

freqtrade/rpc/api_server/api_schemas.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from freqtrade.constants import DL_DATA_TIMEFRAMES, IntOrInf
77
from freqtrade.enums import MarginMode, OrderTypeValues, SignalDirection, TradingMode
8-
from freqtrade.ft_types import ValidExchangesType
8+
from freqtrade.ft_types import AnnotationType, ValidExchangesType
99
from freqtrade.rpc.api_server.webserver_bgwork import ProgressTask
1010

1111

@@ -539,6 +539,7 @@ class PairHistory(BaseModel):
539539
columns: list[str]
540540
all_columns: list[str] = []
541541
data: SerializeAsAny[list[Any]]
542+
annotations: list[AnnotationType] | None = None
542543
length: int
543544
buy_signals: int
544545
sell_signals: int

freqtrade/rpc/rpc.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
from freqtrade.exceptions import ExchangeError, PricingError
3333
from freqtrade.exchange import Exchange, timeframe_to_minutes, timeframe_to_msecs
3434
from freqtrade.exchange.exchange_utils import price_to_precision
35+
from freqtrade.ft_types import AnnotationType
3536
from freqtrade.loggers import bufferHandler
3637
from freqtrade.persistence import CustomDataWrapper, KeyValueStore, PairLocks, Trade
3738
from freqtrade.persistence.models import PairLock
@@ -1356,6 +1357,7 @@ def _convert_dataframe_to_dict(
13561357
dataframe: DataFrame,
13571358
last_analyzed: datetime,
13581359
selected_cols: list[str] | None,
1360+
annotations: list[AnnotationType],
13591361
) -> dict[str, Any]:
13601362
has_content = len(dataframe) != 0
13611363
dataframe_columns = list(dataframe.columns)
@@ -1411,6 +1413,7 @@ def _convert_dataframe_to_dict(
14111413
"data_start_ts": 0,
14121414
"data_stop": "",
14131415
"data_stop_ts": 0,
1416+
"annotations": annotations,
14141417
}
14151418
if has_content:
14161419
res.update(
@@ -1429,8 +1432,16 @@ def _rpc_analysed_dataframe(
14291432
"""Analyzed dataframe in Dict form"""
14301433

14311434
_data, last_analyzed = self.__rpc_analysed_dataframe_raw(pair, timeframe, limit)
1435+
annotations = self._freqtrade.strategy.ft_plot_annotations(pair=pair, dataframe=_data)
1436+
14321437
return RPC._convert_dataframe_to_dict(
1433-
self._freqtrade.config["strategy"], pair, timeframe, _data, last_analyzed, selected_cols
1438+
self._freqtrade.config["strategy"],
1439+
pair,
1440+
timeframe,
1441+
_data,
1442+
last_analyzed,
1443+
selected_cols,
1444+
annotations,
14341445
)
14351446

14361447
def __rpc_analysed_dataframe_raw(
@@ -1531,6 +1542,7 @@ def _rpc_analysed_history_full(
15311542
)
15321543
data = _data[pair]
15331544

1545+
annotations = []
15341546
if config.get("strategy"):
15351547
strategy.dp = DataProvider(config, exchange=exchange, pairlists=None)
15361548
strategy.ft_bot_start()
@@ -1539,6 +1551,8 @@ def _rpc_analysed_history_full(
15391551
df_analyzed = trim_dataframe(
15401552
df_analyzed, timerange_parsed, startup_candles=startup_candles
15411553
)
1554+
annotations = strategy.ft_plot_annotations(pair=pair, dataframe=df_analyzed)
1555+
15421556
else:
15431557
df_analyzed = data
15441558

@@ -1549,6 +1563,7 @@ def _rpc_analysed_history_full(
15491563
df_analyzed.copy(),
15501564
dt_now(),
15511565
selected_cols,
1566+
annotations,
15521567
)
15531568

15541569
def _rpc_plot_config(self) -> dict[str, Any]:

freqtrade/strategy/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
timeframe_to_prev_date,
77
timeframe_to_seconds,
88
)
9+
from freqtrade.ft_types import AnnotationType
910
from freqtrade.persistence import Order, PairLocks, Trade
1011
from freqtrade.strategy.informative_decorator import informative
1112
from freqtrade.strategy.interface import IStrategy
@@ -44,4 +45,5 @@
4445
"merge_informative_pair",
4546
"stoploss_from_absolute",
4647
"stoploss_from_open",
48+
"AnnotationType",
4749
]

freqtrade/strategy/interface.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from math import isinf, isnan
1010

1111
from pandas import DataFrame
12+
from pydantic import ValidationError
1213

1314
from freqtrade.constants import CUSTOM_TAG_MAX_LENGTH, Config, IntOrInf, ListPairsWithTimeframes
1415
from freqtrade.data.converter import populate_dataframe_with_trades
@@ -27,6 +28,7 @@
2728
)
2829
from freqtrade.exceptions import OperationalException, StrategyError
2930
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date, timeframe_to_seconds
31+
from freqtrade.ft_types import AnnotationType
3032
from freqtrade.misc import remove_entry_exit_signals
3133
from freqtrade.persistence import Order, PairLocks, Trade
3234
from freqtrade.strategy.hyper import HyperStrategyMixin
@@ -834,6 +836,24 @@ def version(self) -> str | None:
834836
"""
835837
return None
836838

839+
def plot_annotations(
840+
self, pair: str, start_date: datetime, end_date: datetime, dataframe: DataFrame, **kwargs
841+
) -> list[AnnotationType]:
842+
"""
843+
Retrieve area annotations for a chart.
844+
Must be returned as array, with type, label, color, start, end, y_start, y_end.
845+
All settings except for type are optional - though it usually makes sense to include either
846+
"start and end" or "y_start and y_end" for either horizontal or vertical plots
847+
(or all 4 for boxes).
848+
:param pair: Pair that's currently analyzed
849+
:param start_date: Start date of the chart data being requested
850+
:param end_date: End date of the chart data being requested
851+
:param dataframe: DataFrame with the analyzed data for the chart
852+
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
853+
:return: List of AnnotationType objects
854+
"""
855+
return []
856+
837857
def populate_any_indicators(
838858
self,
839859
pair: str,
@@ -1780,3 +1800,33 @@ def advise_exit(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
17801800
if "exit_long" not in df.columns:
17811801
df = df.rename({"sell": "exit_long"}, axis="columns")
17821802
return df
1803+
1804+
def ft_plot_annotations(self, pair: str, dataframe: DataFrame) -> list[AnnotationType]:
1805+
"""
1806+
Internal wrapper around plot_dataframe
1807+
"""
1808+
if len(dataframe) > 0:
1809+
annotations = strategy_safe_wrapper(self.plot_annotations)(
1810+
pair=pair,
1811+
dataframe=dataframe,
1812+
start_date=dataframe.iloc[0]["date"].to_pydatetime(),
1813+
end_date=dataframe.iloc[-1]["date"].to_pydatetime(),
1814+
)
1815+
1816+
from freqtrade.ft_types.plot_annotation_type import AnnotationTypeTA
1817+
1818+
annotations_new: list[AnnotationType] = []
1819+
for annotation in annotations:
1820+
if isinstance(annotation, dict):
1821+
# Convert to AnnotationType
1822+
try:
1823+
AnnotationTypeTA.validate_python(annotation)
1824+
annotations_new.append(annotation)
1825+
except ValidationError as e:
1826+
logger.error(f"Invalid annotation data: {annotation}. Error: {e}")
1827+
else:
1828+
# Already an AnnotationType
1829+
annotations_new.append(annotation)
1830+
1831+
return annotations_new
1832+
return []

freqtrade/templates/base_strategy.py.j2

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ from freqtrade.strategy import (
2828
merge_informative_pair,
2929
stoploss_from_absolute,
3030
stoploss_from_open,
31+
AnnotationType,
3132
)
3233

3334
# --------------------------------

0 commit comments

Comments
 (0)