Merge pull request #10143 from freqtrade/feat/pairhistory_advanced

RPC: Advanced pairhistory endpoint
This commit is contained in:
Matthias
2024-04-28 20:07:24 +02:00
committed by GitHub
4 changed files with 210 additions and 88 deletions
+14
View File
@@ -489,12 +489,26 @@ class AvailablePairs(BaseModel):
pair_interval: List[List[str]] pair_interval: List[List[str]]
class PairCandlesRequest(BaseModel):
pair: str
timeframe: str
limit: Optional[int] = None
columns: Optional[List[str]] = None
class PairHistoryRequest(PairCandlesRequest):
timerange: str
strategy: str
freqaimodel: Optional[str] = None
class PairHistory(BaseModel): class PairHistory(BaseModel):
strategy: str strategy: str
pair: str pair: str
timeframe: str timeframe: str
timeframe_ms: int timeframe_ms: int
columns: List[str] columns: List[str]
all_columns: List[str] = []
data: SerializeAsAny[List[Any]] data: SerializeAsAny[List[Any]]
length: int length: int
buy_signals: int buy_signals: int
+34 -7
View File
@@ -17,10 +17,11 @@ from freqtrade.rpc.api_server.api_schemas import (AvailablePairs, Balances, Blac
ForceEnterResponse, ForceExitPayload, ForceEnterResponse, ForceExitPayload,
FreqAIModelListResponse, Health, Locks, FreqAIModelListResponse, Health, Locks,
LocksPayload, Logs, MixTag, OpenTradeSchema, LocksPayload, Logs, MixTag, OpenTradeSchema,
PairHistory, PerformanceEntry, Ping, PlotConfig, PairCandlesRequest, PairHistory,
Profit, ResultMsg, ShowConfig, Stats, StatusMsg, PairHistoryRequest, PerformanceEntry, Ping,
StrategyListResponse, StrategyResponse, SysInfo, PlotConfig, Profit, ResultMsg, ShowConfig, Stats,
Version, WhitelistResponse) StatusMsg, StrategyListResponse, StrategyResponse,
SysInfo, Version, WhitelistResponse)
from freqtrade.rpc.api_server.deps import get_config, get_exchange, get_rpc, get_rpc_optional from freqtrade.rpc.api_server.deps import get_config, get_exchange, get_rpc, get_rpc_optional
from freqtrade.rpc.rpc import RPCException from freqtrade.rpc.rpc import RPCException
@@ -53,7 +54,8 @@ logger = logging.getLogger(__name__)
# 2.32: new /backtest/history/ patch endpoint # 2.32: new /backtest/history/ patch endpoint
# 2.33: Additional weekly/monthly metrics # 2.33: Additional weekly/monthly metrics
# 2.34: new entries/exits/mix_tags endpoints # 2.34: new entries/exits/mix_tags endpoints
API_VERSION = 2.34 # 2.35: pair_candles and pair_history endpoints as Post variant
API_VERSION = 2.35
# Public API, requires no auth. # Public API, requires no auth.
router_public = APIRouter() router_public = APIRouter()
@@ -291,7 +293,14 @@ def reload_config(rpc: RPC = Depends(get_rpc)):
@router.get('/pair_candles', response_model=PairHistory, tags=['candle data']) @router.get('/pair_candles', response_model=PairHistory, tags=['candle data'])
def pair_candles( def pair_candles(
pair: str, timeframe: str, limit: Optional[int] = None, rpc: RPC = Depends(get_rpc)): pair: str, timeframe: str, limit: Optional[int] = None, rpc: RPC = Depends(get_rpc)):
return rpc._rpc_analysed_dataframe(pair, timeframe, limit) return rpc._rpc_analysed_dataframe(pair, timeframe, limit, None)
@router.post('/pair_candles', response_model=PairHistory, tags=['candle data'])
def pair_candles_filtered(payload: PairCandlesRequest, rpc: RPC = Depends(get_rpc)):
# Advanced pair_candles endpoint with column filtering
return rpc._rpc_analysed_dataframe(
payload.pair, payload.timeframe, payload.limit, payload.columns)
@router.get('/pair_history', response_model=PairHistory, tags=['candle data']) @router.get('/pair_history', response_model=PairHistory, tags=['candle data'])
@@ -307,7 +316,25 @@ def pair_history(pair: str, timeframe: str, timerange: str, strategy: str,
'freqaimodel': freqaimodel if freqaimodel else config.get('freqaimodel'), 'freqaimodel': freqaimodel if freqaimodel else config.get('freqaimodel'),
}) })
try: try:
return RPC._rpc_analysed_history_full(config, pair, timeframe, exchange) return RPC._rpc_analysed_history_full(config, pair, timeframe, exchange, None)
except Exception as e:
raise HTTPException(status_code=502, detail=str(e))
@router.post('/pair_history', response_model=PairHistory, tags=['candle data'])
def pair_history_filtered(payload: PairHistoryRequest,
config=Depends(get_config), exchange=Depends(get_exchange)):
# The initial call to this endpoint can be slow, as it may need to initialize
# the exchange class.
config = deepcopy(config)
config.update({
'strategy': payload.strategy,
'timerange': payload.timerange,
'freqaimodel': payload.freqaimodel if payload.freqaimodel else config.get('freqaimodel'),
})
try:
return RPC._rpc_analysed_history_full(
config, payload.pair, payload.timeframe, exchange, payload.columns)
except Exception as e: except Exception as e:
raise HTTPException(status_code=502, detail=str(e)) raise HTTPException(status_code=502, detail=str(e))
+21 -9
View File
@@ -16,7 +16,7 @@ from sqlalchemy import func, select
from freqtrade import __version__ from freqtrade import __version__
from freqtrade.configuration.timerange import TimeRange from freqtrade.configuration.timerange import TimeRange
from freqtrade.constants import CANCEL_REASON, Config from freqtrade.constants import CANCEL_REASON, DEFAULT_DATAFRAME_COLUMNS, Config
from freqtrade.data.history import load_data from freqtrade.data.history import load_data
from freqtrade.data.metrics import calculate_expectancy, calculate_max_drawdown from freqtrade.data.metrics import calculate_expectancy, calculate_max_drawdown
from freqtrade.enums import (CandleType, ExitCheckTuple, ExitType, MarketDirection, SignalDirection, from freqtrade.enums import (CandleType, ExitCheckTuple, ExitType, MarketDirection, SignalDirection,
@@ -1190,9 +1190,11 @@ class RPC:
return self._freqtrade.edge.accepted_pairs() return self._freqtrade.edge.accepted_pairs()
@staticmethod @staticmethod
def _convert_dataframe_to_dict(strategy: str, pair: str, timeframe: str, dataframe: DataFrame, def _convert_dataframe_to_dict(
last_analyzed: datetime) -> Dict[str, Any]: strategy: str, pair: str, timeframe: str, dataframe: DataFrame,
last_analyzed: datetime, selected_cols: Optional[List[str]]) -> Dict[str, Any]:
has_content = len(dataframe) != 0 has_content = len(dataframe) != 0
dataframe_columns = list(dataframe.columns)
signals = { signals = {
'enter_long': 0, 'enter_long': 0,
'exit_long': 0, 'exit_long': 0,
@@ -1200,6 +1202,11 @@ class RPC:
'exit_short': 0, 'exit_short': 0,
} }
if has_content: if has_content:
if selected_cols is not None:
# Ensure OHLCV columns are always present
cols_set = set(DEFAULT_DATAFRAME_COLUMNS + list(signals.keys()) + selected_cols)
df_cols = [col for col in dataframe_columns if col in cols_set]
dataframe = dataframe.loc[:, df_cols]
dataframe.loc[:, '__date_ts'] = dataframe.loc[:, 'date'].astype(int64) // 1000 // 1000 dataframe.loc[:, '__date_ts'] = dataframe.loc[:, 'date'].astype(int64) // 1000 // 1000
# Move signal close to separate column when signal for easy plotting # Move signal close to separate column when signal for easy plotting
@@ -1224,6 +1231,7 @@ class RPC:
'timeframe': timeframe, 'timeframe': timeframe,
'timeframe_ms': timeframe_to_msecs(timeframe), 'timeframe_ms': timeframe_to_msecs(timeframe),
'strategy': strategy, 'strategy': strategy,
'all_columns': dataframe_columns,
'columns': list(dataframe.columns), 'columns': list(dataframe.columns),
'data': dataframe.values.tolist(), 'data': dataframe.values.tolist(),
'length': len(dataframe), 'length': len(dataframe),
@@ -1249,13 +1257,16 @@ class RPC:
}) })
return res return res
def _rpc_analysed_dataframe(self, pair: str, timeframe: str, def _rpc_analysed_dataframe(
limit: Optional[int]) -> Dict[str, Any]: self, pair: str, timeframe: str, limit: Optional[int],
selected_cols: Optional[List[str]]) -> Dict[str, Any]:
""" 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)
return RPC._convert_dataframe_to_dict(self._freqtrade.config['strategy'], return RPC._convert_dataframe_to_dict(
pair, timeframe, _data, last_analyzed) self._freqtrade.config['strategy'], pair, timeframe, _data, last_analyzed,
selected_cols
)
def __rpc_analysed_dataframe_raw( def __rpc_analysed_dataframe_raw(
self, self,
@@ -1322,7 +1333,7 @@ class RPC:
@staticmethod @staticmethod
def _rpc_analysed_history_full(config: Config, pair: str, timeframe: str, def _rpc_analysed_history_full(config: Config, pair: str, timeframe: str,
exchange) -> Dict[str, Any]: exchange, selected_cols: Optional[List[str]]) -> Dict[str, Any]:
timerange_parsed = TimeRange.parse_timerange(config.get('timerange')) timerange_parsed = TimeRange.parse_timerange(config.get('timerange'))
from freqtrade.data.converter import trim_dataframe from freqtrade.data.converter import trim_dataframe
@@ -1352,7 +1363,8 @@ class RPC:
df_analyzed = trim_dataframe(df_analyzed, timerange_parsed, startup_candles=startup_candles) df_analyzed = trim_dataframe(df_analyzed, timerange_parsed, startup_candles=startup_candles)
return RPC._convert_dataframe_to_dict(strategy.get_strategy_name(), pair, timeframe, return RPC._convert_dataframe_to_dict(strategy.get_strategy_name(), pair, timeframe,
df_analyzed.copy(), dt_now()) df_analyzed.copy(), dt_now(),
selected_cols)
def _rpc_plot_config(self) -> Dict[str, Any]: def _rpc_plot_config(self) -> Dict[str, Any]:
if (self._freqtrade.strategy.plot_config and if (self._freqtrade.strategy.plot_config and
+103 -34
View File
@@ -1511,6 +1511,7 @@ def test_api_pair_candles(botclient, ohlcv_history):
assert 'data_stop_ts' in rc.json() assert 'data_stop_ts' in rc.json()
assert len(rc.json()['data']) == 0 assert len(rc.json()['data']) == 0
ohlcv_history['sma'] = ohlcv_history['close'].rolling(2).mean() ohlcv_history['sma'] = ohlcv_history['close'].rolling(2).mean()
ohlcv_history['sma2'] = ohlcv_history['close'].rolling(2).mean()
ohlcv_history['enter_long'] = 0 ohlcv_history['enter_long'] = 0
ohlcv_history.loc[1, 'enter_long'] = 1 ohlcv_history.loc[1, 'enter_long'] = 1
ohlcv_history['exit_long'] = 0 ohlcv_history['exit_long'] = 0
@@ -1518,44 +1519,83 @@ 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)
for call in ('get', 'post'):
rc = client_get(client, if call == 'get':
rc = client_get(
client,
f"{BASE_URI}/pair_candles?limit={amount}&pair=XRP%2FBTC&timeframe={timeframe}") f"{BASE_URI}/pair_candles?limit={amount}&pair=XRP%2FBTC&timeframe={timeframe}")
else:
rc = client_post(
client,
f"{BASE_URI}/pair_candles",
data={
"pair": "XRP/BTC",
"timeframe": timeframe,
"limit": amount,
"columns": ['sma'],
}
)
assert_response(rc) assert_response(rc)
assert 'strategy' in rc.json() resp = rc.json()
assert rc.json()['strategy'] == CURRENT_TEST_STRATEGY assert 'strategy' in resp
assert 'columns' in rc.json() assert resp['strategy'] == CURRENT_TEST_STRATEGY
assert 'data_start_ts' in rc.json() assert 'columns' in resp
assert 'data_start' in rc.json() assert 'data_start_ts' in resp
assert 'data_stop' in rc.json() assert 'data_start' in resp
assert 'data_stop_ts' in rc.json() assert 'data_stop' in resp
assert rc.json()['data_start'] == '2017-11-26 08:50:00+00:00' assert 'data_stop_ts' in resp
assert rc.json()['data_start_ts'] == 1511686200000 assert resp['data_start'] == '2017-11-26 08:50:00+00:00'
assert rc.json()['data_stop'] == '2017-11-26 09:00:00+00:00' assert resp['data_start_ts'] == 1511686200000
assert rc.json()['data_stop_ts'] == 1511686800000 assert resp['data_stop'] == '2017-11-26 09:00:00+00:00'
assert isinstance(rc.json()['columns'], list) assert resp['data_stop_ts'] == 1511686800000
assert set(rc.json()['columns']) == { assert isinstance(resp['columns'], list)
base_cols = {
'date', 'open', 'high', 'low', 'close', 'volume', 'date', 'open', 'high', 'low', 'close', 'volume',
'sma', 'enter_long', 'exit_long', 'enter_short', 'exit_short', '__date_ts', 'sma', 'enter_long', 'exit_long', 'enter_short', 'exit_short', '__date_ts',
'_enter_long_signal_close', '_exit_long_signal_close', '_enter_long_signal_close', '_exit_long_signal_close',
'_enter_short_signal_close', '_exit_short_signal_close' '_enter_short_signal_close', '_exit_short_signal_close'
} }
assert 'pair' in rc.json() if call == 'get':
assert rc.json()['pair'] == 'XRP/BTC' assert set(resp['columns']) == base_cols.union({'sma2'})
else:
assert set(resp['columns']) == base_cols
assert 'data' in rc.json() # All columns doesn't include the internal columns
assert len(rc.json()['data']) == amount assert set(resp['all_columns']) == {
'date', 'open', 'high', 'low', 'close', 'volume',
'sma', 'sma2', 'enter_long', 'exit_long', 'enter_short', 'exit_short'
}
assert 'pair' in resp
assert resp['pair'] == 'XRP/BTC'
assert (rc.json()['data'] == assert 'data' in resp
[['2017-11-26T08:50:00Z', 8.794e-05, 8.948e-05, 8.794e-05, 8.88e-05, 0.0877869, assert len(resp['data']) == amount
None, 0, 0, 0, 0, 1511686200000, None, None, None, None], if call == 'get':
['2017-11-26T08:55:00Z', 8.88e-05, 8.942e-05, 8.88e-05, assert len(resp['data'][0]) == 17
8.893e-05, 0.05874751, 8.886500000000001e-05, 1, 0, 0, 0, 1511686500000, 8.893e-05, assert resp['data'] == [
None, None, None], ['2017-11-26T08:50:00Z', 8.794e-05, 8.948e-05, 8.794e-05, 8.88e-05,
0.0877869, None, None, 0, 0, 0, 0, 1511686200000, None, None, None, None],
['2017-11-26T08:55:00Z', 8.88e-05, 8.942e-05, 8.88e-05, 8.893e-05, 0.05874751,
8.886500000000001e-05, 8.886500000000001e-05, 1, 0, 0, 0, 1511686500000,
8.893e-05, None, None, None],
['2017-11-26T09:00:00Z', 8.891e-05, 8.893e-05, 8.875e-05, 8.877e-05, ['2017-11-26T09:00:00Z', 8.891e-05, 8.893e-05, 8.875e-05, 8.877e-05,
0.7039405, 8.885e-05, 0, 0, 0, 0, 1511686800000, None, None, None, None] 0.7039405, 8.885e-05, 8.885e-05, 0, 0, 0, 0, 1511686800000, None, None, None, None
]
]
else:
assert len(resp['data'][0]) == 16
assert resp['data'] == [
['2017-11-26T08:50:00Z', 8.794e-05, 8.948e-05, 8.794e-05, 8.88e-05,
0.0877869, None, 0, 0, 0, 0, 1511686200000, None, None, None, None],
['2017-11-26T08:55:00Z', 8.88e-05, 8.942e-05, 8.88e-05, 8.893e-05, 0.05874751,
8.886500000000001e-05, 1, 0, 0, 0, 1511686500000,
8.893e-05, None, None, None],
['2017-11-26T09:00:00Z', 8.891e-05, 8.893e-05, 8.875e-05, 8.877e-05,
0.7039405, 8.885e-05, 0, 0, 0, 0, 1511686800000, None, None, None, None
]
]
]) # prep for next test
ohlcv_history['exit_long'] = ohlcv_history['exit_long'].astype('float64') ohlcv_history['exit_long'] = ohlcv_history['exit_long'].astype('float64')
ohlcv_history.at[0, 'exit_long'] = float('inf') ohlcv_history.at[0, 'exit_long'] = float('inf')
ohlcv_history['date1'] = ohlcv_history['date'] ohlcv_history['date1'] = ohlcv_history['date']
@@ -1567,13 +1607,13 @@ def test_api_pair_candles(botclient, ohlcv_history):
assert_response(rc) assert_response(rc)
assert (rc.json()['data'] == assert (rc.json()['data'] ==
[['2017-11-26T08:50:00Z', 8.794e-05, 8.948e-05, 8.794e-05, 8.88e-05, 0.0877869, [['2017-11-26T08:50:00Z', 8.794e-05, 8.948e-05, 8.794e-05, 8.88e-05, 0.0877869,
None, 0, None, 0, 0, None, 1511686200000, None, None, None, None], None, None, 0, None, 0, 0, None, 1511686200000, None, None, None, None],
['2017-11-26T08:55:00Z', 8.88e-05, 8.942e-05, 8.88e-05, ['2017-11-26T08:55:00Z', 8.88e-05, 8.942e-05, 8.88e-05,
8.893e-05, 0.05874751, 8.886500000000001e-05, 1, 0.0, 0, 0, '2017-11-26T08:55:00Z', 8.893e-05, 0.05874751, 8.886500000000001e-05, 8.886500000000001e-05, 1, 0.0, 0,
1511686500000, 8.893e-05, None, None, None], 0, '2017-11-26T08:55:00Z', 1511686500000, 8.893e-05, None, None, None],
['2017-11-26T09:00:00Z', 8.891e-05, 8.893e-05, 8.875e-05, 8.877e-05, ['2017-11-26T09:00:00Z', 8.891e-05, 8.893e-05, 8.875e-05, 8.877e-05,
0.7039405, 8.885e-05, 0, 0.0, 0, 0, '2017-11-26T09:00:00Z', 1511686800000, 0.7039405, 8.885e-05, 8.885e-05, 0, 0.0, 0, 0, '2017-11-26T09:00:00Z',
None, None, None, None] 1511686800000, None, None, None, None]
]) ])
@@ -1614,9 +1654,23 @@ def test_api_pair_history(botclient, tmp_path, mocker):
assert_response(rc, 502) assert_response(rc, 502)
# Working # Working
for call in ('get', 'post'):
if call == 'get':
rc = client_get(client, rc = client_get(client,
f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC&timeframe={timeframe}" f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC&timeframe={timeframe}"
f"&timerange=20180111-20180112&strategy={CURRENT_TEST_STRATEGY}") f"&timerange=20180111-20180112&strategy={CURRENT_TEST_STRATEGY}")
else:
rc = client_post(
client,
f"{BASE_URI}/pair_history",
data={
"pair": "UNITTEST/BTC",
"timeframe": timeframe,
"timerange": "20180111-20180112",
"strategy": CURRENT_TEST_STRATEGY,
"columns": ['rsi', 'fastd', 'fastk'],
})
assert_response(rc, 200) assert_response(rc, 200)
result = rc.json() result = rc.json()
assert result['length'] == 289 assert result['length'] == 289
@@ -1625,9 +1679,11 @@ def test_api_pair_history(botclient, tmp_path, mocker):
assert 'data' in result assert 'data' in result
data = result['data'] data = result['data']
assert len(data) == 289 assert len(data) == 289
col_count = 30 if call == 'get' else 18
# analyzed DF has 30 columns # analyzed DF has 30 columns
assert len(result['columns']) == 30 assert len(result['columns']) == col_count
assert len(data[0]) == 30 assert len(result['all_columns']) == 25
assert len(data[0]) == col_count
date_col_idx = [idx for idx, c in enumerate(result['columns']) if c == 'date'][0] date_col_idx = [idx for idx, c in enumerate(result['columns']) if c == 'date'][0]
rsi_col_idx = [idx for idx, c in enumerate(result['columns']) if c == 'rsi'][0] rsi_col_idx = [idx for idx, c in enumerate(result['columns']) if c == 'rsi'][0]
@@ -1641,11 +1697,24 @@ 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
lfm.reset_mock()
# No data found # No data found
if call == 'get':
rc = client_get(client, rc = client_get(client,
f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC&timeframe={timeframe}" f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC&timeframe={timeframe}"
f"&timerange=20200111-20200112&strategy={CURRENT_TEST_STRATEGY}") f"&timerange=20200111-20200112&strategy={CURRENT_TEST_STRATEGY}")
else:
rc = client_post(
client,
f"{BASE_URI}/pair_history",
data={
"pair": "UNITTEST/BTC",
"timeframe": timeframe,
"timerange": "20200111-20200112",
"strategy": CURRENT_TEST_STRATEGY,
"columns": ['rsi', 'fastd', 'fastk'],
})
assert_response(rc, 502) assert_response(rc, 502)
assert rc.json()['detail'] == ("No data for UNITTEST/BTC, 5m in 20200111-20200112 found.") assert rc.json()['detail'] == ("No data for UNITTEST/BTC, 5m in 20200111-20200112 found.")