diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index f1ac9db54..892865d43 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -1,7 +1,7 @@ from datetime import date, datetime from typing import Any, Dict, List, Optional, Union -from pydantic import BaseModel +from pydantic import BaseModel, ConfigDict, RootModel, SerializeAsAny from freqtrade.constants import DATETIME_PRINT_FORMAT, IntOrInf from freqtrade.enums import MarginMode, OrderTypeValues, SignalDirection, TradingMode @@ -9,9 +9,9 @@ from freqtrade.types import ValidExchangesType class ExchangeModePayloadMixin(BaseModel): - trading_mode: Optional[TradingMode] - margin_mode: Optional[MarginMode] - exchange: Optional[str] + trading_mode: Optional[TradingMode] = None + margin_mode: Optional[MarginMode] = None + exchange: Optional[str] = None class Ping(BaseModel): @@ -43,11 +43,11 @@ class BackgroundTaskStatus(BaseModel): job_category: str status: str running: bool - progress: Optional[float] + progress: Optional[float] = None class BackgroundTaskResult(BaseModel): - error: Optional[str] + error: Optional[str] = None status: str @@ -60,9 +60,9 @@ class Balance(BaseModel): free: float balance: float used: float - bot_owned: Optional[float] + bot_owned: Optional[float] = None est_stake: float - est_stake_bot: Optional[float] + est_stake_bot: Optional[float] = None stake: str # Starting with 2.x side: str @@ -141,7 +141,7 @@ class Profit(BaseModel): expectancy_ratio: float max_drawdown: float max_drawdown_abs: float - trading_volume: Optional[float] + trading_volume: Optional[float] = None bot_start_timestamp: int bot_start_date: str @@ -173,50 +173,50 @@ class Daily(BaseModel): class UnfilledTimeout(BaseModel): - entry: Optional[int] - exit: Optional[int] - unit: Optional[str] - exit_timeout_count: Optional[int] + entry: Optional[int] = None + exit: Optional[int] = None + unit: Optional[str] = None + exit_timeout_count: Optional[int] = None class OrderTypes(BaseModel): entry: OrderTypeValues exit: OrderTypeValues - emergency_exit: Optional[OrderTypeValues] - force_exit: Optional[OrderTypeValues] - force_entry: Optional[OrderTypeValues] + emergency_exit: Optional[OrderTypeValues] = None + force_exit: Optional[OrderTypeValues] = None + force_entry: Optional[OrderTypeValues] = None stoploss: OrderTypeValues stoploss_on_exchange: bool - stoploss_on_exchange_interval: Optional[int] + stoploss_on_exchange_interval: Optional[int] = None class ShowConfig(BaseModel): version: str - strategy_version: Optional[str] + strategy_version: Optional[str] = None api_version: float dry_run: bool trading_mode: str short_allowed: bool stake_currency: str stake_amount: str - available_capital: Optional[float] + available_capital: Optional[float] = None stake_currency_decimals: int max_open_trades: IntOrInf minimal_roi: Dict[str, Any] - stoploss: Optional[float] + stoploss: Optional[float] = None stoploss_on_exchange: bool - trailing_stop: Optional[bool] - trailing_stop_positive: Optional[float] - trailing_stop_positive_offset: Optional[float] - trailing_only_offset_is_reached: Optional[bool] - unfilledtimeout: Optional[UnfilledTimeout] # Empty in webserver mode - order_types: Optional[OrderTypes] - use_custom_stoploss: Optional[bool] - timeframe: Optional[str] + trailing_stop: Optional[bool] = None + trailing_stop_positive: Optional[float] = None + trailing_stop_positive_offset: Optional[float] = None + trailing_only_offset_is_reached: Optional[bool] = None + unfilledtimeout: Optional[UnfilledTimeout] = None # Empty in webserver mode + order_types: Optional[OrderTypes] = None + use_custom_stoploss: Optional[bool] = None + timeframe: Optional[str] = None timeframe_ms: int timeframe_min: int exchange: str - strategy: Optional[str] + strategy: Optional[str] = None force_entry_enable: bool exit_pricing: Dict[str, Any] entry_pricing: Dict[str, Any] @@ -231,17 +231,17 @@ class OrderSchema(BaseModel): pair: str order_id: str status: str - remaining: Optional[float] + remaining: Optional[float] = None amount: float safe_price: float cost: float - filled: Optional[float] + filled: Optional[float] = None ft_order_side: str order_type: str is_open: bool - order_timestamp: Optional[int] - order_filled_timestamp: Optional[int] - ft_fee_base: Optional[float] + order_timestamp: Optional[int] = None + order_filled_timestamp: Optional[int] = None + ft_fee_base: Optional[float] = None class TradeSchema(BaseModel): @@ -255,81 +255,81 @@ class TradeSchema(BaseModel): amount: float amount_requested: float stake_amount: float - max_stake_amount: Optional[float] + max_stake_amount: Optional[float] = None strategy: str - enter_tag: Optional[str] + enter_tag: Optional[str] = None timeframe: int - fee_open: Optional[float] - fee_open_cost: Optional[float] - fee_open_currency: Optional[str] - fee_close: Optional[float] - fee_close_cost: Optional[float] - fee_close_currency: Optional[str] + fee_open: Optional[float] = None + fee_open_cost: Optional[float] = None + fee_open_currency: Optional[str] = None + fee_close: Optional[float] = None + fee_close_cost: Optional[float] = None + fee_close_currency: Optional[str] = None open_date: str open_timestamp: int open_rate: float - open_rate_requested: Optional[float] + open_rate_requested: Optional[float] = None open_trade_value: float - close_date: Optional[str] - close_timestamp: Optional[int] - close_rate: Optional[float] - close_rate_requested: Optional[float] + close_date: Optional[str] = None + close_timestamp: Optional[int] = None + close_rate: Optional[float] = None + close_rate_requested: Optional[float] = None - close_profit: Optional[float] - close_profit_pct: Optional[float] - close_profit_abs: Optional[float] + close_profit: Optional[float] = None + close_profit_pct: Optional[float] = None + close_profit_abs: Optional[float] = None - profit_ratio: Optional[float] - profit_pct: Optional[float] - profit_abs: Optional[float] - profit_fiat: Optional[float] + profit_ratio: Optional[float] = None + profit_pct: Optional[float] = None + profit_abs: Optional[float] = None + profit_fiat: Optional[float] = None realized_profit: float - realized_profit_ratio: Optional[float] + realized_profit_ratio: Optional[float] = None - exit_reason: Optional[str] - exit_order_status: Optional[str] + exit_reason: Optional[str] = None + exit_order_status: Optional[str] = None - stop_loss_abs: Optional[float] - stop_loss_ratio: Optional[float] - stop_loss_pct: Optional[float] - stoploss_order_id: Optional[str] - stoploss_last_update: Optional[str] - stoploss_last_update_timestamp: Optional[int] - initial_stop_loss_abs: Optional[float] - initial_stop_loss_ratio: Optional[float] - initial_stop_loss_pct: Optional[float] + stop_loss_abs: Optional[float] = None + stop_loss_ratio: Optional[float] = None + stop_loss_pct: Optional[float] = None + stoploss_order_id: Optional[str] = None + stoploss_last_update: Optional[str] = None + stoploss_last_update_timestamp: Optional[int] = None + initial_stop_loss_abs: Optional[float] = None + initial_stop_loss_ratio: Optional[float] = None + initial_stop_loss_pct: Optional[float] = None - min_rate: Optional[float] - max_rate: Optional[float] - open_order_id: Optional[str] + min_rate: Optional[float] = None + max_rate: Optional[float] = None + open_order_id: Optional[str] = None orders: List[OrderSchema] - leverage: Optional[float] - interest_rate: Optional[float] - liquidation_price: Optional[float] - funding_fees: Optional[float] - trading_mode: Optional[TradingMode] + leverage: Optional[float] = None + interest_rate: Optional[float] = None + liquidation_price: Optional[float] = None + funding_fees: Optional[float] = None + trading_mode: Optional[TradingMode] = None - amount_precision: Optional[float] - price_precision: Optional[float] - precision_mode: Optional[int] + amount_precision: Optional[float] = None + price_precision: Optional[float] = None + precision_mode: Optional[int] = None class OpenTradeSchema(TradeSchema): - stoploss_current_dist: Optional[float] - stoploss_current_dist_pct: Optional[float] - stoploss_current_dist_ratio: Optional[float] - stoploss_entry_dist: Optional[float] - stoploss_entry_dist_ratio: Optional[float] + stoploss_current_dist: Optional[float] = None + stoploss_current_dist_pct: Optional[float] = None + stoploss_current_dist_ratio: Optional[float] = None + stoploss_entry_dist: Optional[float] = None + stoploss_entry_dist_ratio: Optional[float] = None current_rate: float total_profit_abs: float - total_profit_fiat: Optional[float] - total_profit_ratio: Optional[float] + total_profit_fiat: Optional[float] = None + total_profit_ratio: Optional[float] = None - open_order: Optional[str] + open_order: Optional[str] = None class TradeResponse(BaseModel): @@ -339,8 +339,7 @@ class TradeResponse(BaseModel): total_trades: int -class ForceEnterResponse(BaseModel): - __root__: Union[TradeSchema, StatusMsg] +ForceEnterResponse = RootModel[Union[TradeSchema, StatusMsg]] class LockModel(BaseModel): @@ -352,7 +351,7 @@ class LockModel(BaseModel): lock_timestamp: int pair: str side: str - reason: Optional[str] + reason: Optional[str] = None class Locks(BaseModel): @@ -361,8 +360,8 @@ class Locks(BaseModel): class DeleteLockRequest(BaseModel): - pair: Optional[str] - lockid: Optional[int] + pair: Optional[str] = None + lockid: Optional[int] = None class Logs(BaseModel): @@ -373,17 +372,17 @@ class Logs(BaseModel): class ForceEnterPayload(BaseModel): pair: str side: SignalDirection = SignalDirection.LONG - price: Optional[float] - ordertype: Optional[OrderTypeValues] - stakeamount: Optional[float] - entry_tag: Optional[str] - leverage: Optional[float] + price: Optional[float] = None + ordertype: Optional[OrderTypeValues] = None + stakeamount: Optional[float] = None + entry_tag: Optional[str] = None + leverage: Optional[float] = None class ForceExitPayload(BaseModel): tradeid: str - ordertype: Optional[OrderTypeValues] - amount: Optional[float] + ordertype: Optional[OrderTypeValues] = None + amount: Optional[float] = None class BlacklistPayload(BaseModel): @@ -405,7 +404,7 @@ class WhitelistResponse(BaseModel): class WhitelistEvaluateResponse(BackgroundTaskResult): - result: Optional[WhitelistResponse] + result: Optional[WhitelistResponse] = None class DeleteTrade(BaseModel): @@ -420,8 +419,7 @@ class PlotConfig_(BaseModel): subplots: Dict[str, Any] -class PlotConfig(BaseModel): - __root__: Union[PlotConfig_, Dict] +PlotConfig = RootModel[Union[PlotConfig_, Dict]] class StrategyListResponse(BaseModel): @@ -470,7 +468,7 @@ class PairHistory(BaseModel): timeframe: str timeframe_ms: int columns: List[str] - data: List[Any] + data: SerializeAsAny[List[Any]] length: int buy_signals: int sell_signals: int @@ -484,11 +482,11 @@ class PairHistory(BaseModel): data_start: str data_stop: str data_stop_ts: int - - class Config: - json_encoders = { - datetime: lambda v: v.strftime(DATETIME_PRINT_FORMAT), - } + # TODO[pydantic]: The following keys were removed: `json_encoders`. + # Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-config for more information. + model_config = ConfigDict(json_encoders={ + datetime: lambda v: v.strftime(DATETIME_PRINT_FORMAT), + }) class BacktestFreqAIInputs(BaseModel): @@ -497,16 +495,16 @@ class BacktestFreqAIInputs(BaseModel): class BacktestRequest(BaseModel): strategy: str - timeframe: Optional[str] - timeframe_detail: Optional[str] - timerange: Optional[str] - max_open_trades: Optional[IntOrInf] - stake_amount: Optional[str] + timeframe: Optional[str] = None + timeframe_detail: Optional[str] = None + timerange: Optional[str] = None + max_open_trades: Optional[IntOrInf] = None + stake_amount: Optional[Union[str, float]] = None enable_protections: bool - dry_run_wallet: Optional[float] - backtest_cache: Optional[str] - freqaimodel: Optional[str] - freqai: Optional[BacktestFreqAIInputs] + dry_run_wallet: Optional[float] = None + backtest_cache: Optional[str] = None + freqaimodel: Optional[str] = None + freqai: Optional[BacktestFreqAIInputs] = None class BacktestResponse(BaseModel): @@ -515,9 +513,9 @@ class BacktestResponse(BaseModel): status_msg: str step: str progress: float - trade_count: Optional[float] + trade_count: Optional[float] = None # TODO: Properly type backtestresult... - backtest_result: Optional[Dict[str, Any]] + backtest_result: Optional[Dict[str, Any]] = None # TODO: This is a copy of BacktestHistoryEntryType @@ -540,5 +538,5 @@ class SysInfo(BaseModel): class Health(BaseModel): - last_process: Optional[datetime] - last_process_ts: Optional[int] + last_process: Optional[datetime] = None + last_process_ts: Optional[int] = None diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index bc0c88fe4..7299364a8 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -175,9 +175,9 @@ def force_entry(payload: ForceEnterPayload, rpc: RPC = Depends(get_rpc)): leverage=payload.leverage) if trade: - return ForceEnterResponse.parse_obj(trade.to_json()) + return ForceEnterResponse.model_validate(trade.to_json()) else: - return ForceEnterResponse.parse_obj( + return ForceEnterResponse.model_validate( {"status": f"Error entering {payload.side} trade for pair {payload.pair}."}) @@ -282,14 +282,14 @@ def plot_config(strategy: Optional[str] = None, config=Depends(get_config), if not strategy: if not rpc: raise RPCException("Strategy is mandatory in webserver mode.") - return PlotConfig.parse_obj(rpc._rpc_plot_config()) + return PlotConfig.model_validate(rpc._rpc_plot_config()) else: config1 = deepcopy(config) config1.update({ 'strategy': strategy }) try: - return PlotConfig.parse_obj(RPC._rpc_plot_config_with_strategy(config1)) + return PlotConfig.model_validate(RPC._rpc_plot_config_with_strategy(config1)) except Exception as e: raise HTTPException(status_code=502, detail=str(e)) diff --git a/freqtrade/rpc/api_server/api_ws.py b/freqtrade/rpc/api_server/api_ws.py index 40a5a75fd..16aeb56f3 100644 --- a/freqtrade/rpc/api_server/api_ws.py +++ b/freqtrade/rpc/api_server/api_ws.py @@ -65,7 +65,7 @@ async def _process_consumer_request( """ # Validate the request, makes sure it matches the schema try: - websocket_request = WSRequestSchema.parse_obj(request) + websocket_request = WSRequestSchema.model_validate(request) except ValidationError as e: logger.error(f"Invalid request from {channel}: {e}") return @@ -94,7 +94,7 @@ async def _process_consumer_request( # Format response response = WSWhitelistMessage(data=whitelist) - await channel.send(response.dict(exclude_none=True)) + await channel.send(response.model_dump(exclude_none=True)) elif type_ == RPCRequestType.ANALYZED_DF: # Limit the amount of candles per dataframe to 'limit' or 1500 @@ -105,7 +105,7 @@ async def _process_consumer_request( for message in rpc._ws_request_analyzed_df(limit, pair): # Format response response = WSAnalyzedDFMessage(data=message) - await channel.send(response.dict(exclude_none=True)) + await channel.send(response.model_dump(exclude_none=True)) @router.websocket("/message/ws") diff --git a/freqtrade/rpc/api_server/ws_schemas.py b/freqtrade/rpc/api_server/ws_schemas.py index af98bd532..34eaf0245 100644 --- a/freqtrade/rpc/api_server/ws_schemas.py +++ b/freqtrade/rpc/api_server/ws_schemas.py @@ -2,15 +2,14 @@ from datetime import datetime from typing import Any, Dict, List, Optional, TypedDict from pandas import DataFrame -from pydantic import BaseModel +from pydantic import BaseModel, ConfigDict from freqtrade.constants import PairWithTimeframe from freqtrade.enums.rpcmessagetype import RPCMessageType, RPCRequestType class BaseArbitraryModel(BaseModel): - class Config: - arbitrary_types_allowed = True + model_config = ConfigDict(arbitrary_types_allowed=True) class WSRequestSchema(BaseArbitraryModel): @@ -27,9 +26,7 @@ class WSMessageSchemaType(TypedDict): class WSMessageSchema(BaseArbitraryModel): type: RPCMessageType data: Optional[Any] = None - - class Config: - extra = 'allow' + model_config = ConfigDict(extra='allow') # ------------------------------ REQUEST SCHEMAS ---------------------------- diff --git a/freqtrade/rpc/external_message_consumer.py b/freqtrade/rpc/external_message_consumer.py index e888191ea..200d408e6 100644 --- a/freqtrade/rpc/external_message_consumer.py +++ b/freqtrade/rpc/external_message_consumer.py @@ -41,7 +41,7 @@ logger = logging.getLogger(__name__) def schema_to_dict(schema: Union[WSMessageSchema, WSRequestSchema]): - return schema.dict(exclude_none=True) + return schema.model_dump(exclude_none=True) class ExternalMessageConsumer: @@ -322,7 +322,7 @@ class ExternalMessageConsumer: producer_name = producer.get('name', 'default') try: - producer_message = WSMessageSchema.parse_obj(message) + producer_message = WSMessageSchema.model_validate(message) except ValidationError as e: logger.error(f"Invalid message from `{producer_name}`: {e}") return @@ -344,7 +344,7 @@ class ExternalMessageConsumer: def _consume_whitelist_message(self, producer_name: str, message: WSMessageSchema): try: # Validate the message - whitelist_message = WSWhitelistMessage.parse_obj(message) + whitelist_message = WSWhitelistMessage.model_validate(message.model_dump()) except ValidationError as e: logger.error(f"Invalid message from `{producer_name}`: {e}") return @@ -356,7 +356,7 @@ class ExternalMessageConsumer: def _consume_analyzed_df_message(self, producer_name: str, message: WSMessageSchema): try: - df_message = WSAnalyzedDFMessage.parse_obj(message) + df_message = WSAnalyzedDFMessage.model_validate(message.model_dump()) except ValidationError as e: logger.error(f"Invalid message from `{producer_name}`: {e}") return diff --git a/requirements.txt b/requirements.txt index 4d1d7906a..8a5a51c6c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -40,7 +40,7 @@ sdnotify==0.3.2 # API Server fastapi==0.101.0 -pydantic==1.10.11 +pydantic==2.2.0 uvicorn==0.23.2 pyjwt==2.8.0 aiofiles==23.2.1 diff --git a/setup.py b/setup.py index c2b725f87..2e92d0839 100644 --- a/setup.py +++ b/setup.py @@ -97,7 +97,7 @@ setup( 'rich', 'pyarrow; platform_machine != "armv7l"', 'fastapi', - 'pydantic>=1.8.0,<2.0', + 'pydantic>=2.2.0', 'uvicorn', 'psutil', 'pyjwt', diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 0c9c964cf..f9533e893 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -1429,12 +1429,12 @@ def test_api_pair_candles(botclient, ohlcv_history): assert len(rc.json()['data']) == amount assert (rc.json()['data'] == - [['2017-11-26 08:50:00', 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, 0, 0, 0, 1511686200000, None, None, None, None], - ['2017-11-26 08:55:00', 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, 1511686500000, 8.893e-05, None, None, None], - ['2017-11-26 09:00:00', 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] ]) @@ -1448,13 +1448,13 @@ def test_api_pair_candles(botclient, ohlcv_history): f"{BASE_URI}/pair_candles?limit={amount}&pair=XRP%2FBTC&timeframe={timeframe}") assert_response(rc) assert (rc.json()['data'] == - [['2017-11-26 08:50:00', 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], - ['2017-11-26 08:55:00', 8.88e-05, 8.942e-05, 8.88e-05, - 8.893e-05, 0.05874751, 8.886500000000001e-05, 1, 0.0, 0, 0, '2017-11-26 08:55:00', + ['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', 1511686500000, 8.893e-05, None, None, None], - ['2017-11-26 09:00:00', 8.891e-05, 8.893e-05, 8.875e-05, 8.877e-05, - 0.7039405, 8.885e-05, 0, 0.0, 0, 0, '2017-11-26 09:00:00', 1511686800000, + ['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, None, None, None, None] ]) @@ -1511,7 +1511,7 @@ def test_api_pair_history(botclient, mocker): 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] - assert data[0][date_col_idx] == '2018-01-11 00:00:00' + assert data[0][date_col_idx] == '2018-01-11T00:00:00Z' assert data[0][rsi_col_idx] is not None assert data[0][rsi_col_idx] > 0 assert lfm.call_count == 1