Skip to content

Commit ade0b0c

Browse files
committed
Implement database synchronization for backtests, plus some new backtest features such as notes, and strategy snapshots
- Streamlined the process of preparing chart data by directly assigning formatted values to the chart_data dictionary. - Removed redundant variable assignments to enhance code clarity and maintainability. Add backtest session management features - Introduced new API endpoints for managing backtest sessions, including fetching all sessions, retrieving a session by ID, updating session state, and removing sessions. - Created a new BacktestSession model to handle session data in the database. - Implemented functions for storing, updating, and deleting backtest sessions. - Enhanced the backtest execution process to log session status and exceptions in the database. - Updated transformers to format backtest session data for API responses. Add clean_infinite_values function and enhance backtest session API - Introduced clean_infinite_values function to recursively clean infinite values from data structures, replacing them with None or 0. - Updated get_backtest_sessions endpoint to support pagination through request body. - Modified get_backtest_sessions function to accept limit and offset parameters for improved session retrieval. - Enhanced data transformation in get_backtest_session_for_load_more to clean infinite values from metrics, equity curve, trades, hyperparameters, and chart data. - Added GetBacktestSessionsRequestJson model for structured request handling in the API. Update backtest session status on cancellation request - Added functionality to update the status of a backtest session to 'cancelled' when a cancellation request is processed. - Enhanced the backtest_controller to import and utilize the update_backtest_session_status function for managing session states. Add backtest session notes management and enhance session model - Introduced a new API endpoint to update notes (title, description, strategy codes) for backtest sessions. - Enhanced the BacktestSession model to include fields for user notes. - Updated the update_backtest_session_results function to handle strategy codes. - Modified transformers to include notes in the session data returned by the API. - Added a new request model for updating backtest session notes. Enhance backtest session retrieval with additional filters - Updated the get_backtest_sessions function to support title search, status filtering, and date filtering for improved session management. - Modified the GetBacktestSessionsRequestJson model to include new optional parameters for filtering. - Enhanced the backtest_controller to utilize the updated session retrieval logic with pagination and filters. Add purge sessions functionality to backtest controller and model - Introduced a new API endpoint to purge backtest sessions older than a specified number of days. - Implemented the purge_backtest_sessions function in the BacktestSession model to handle the deletion of old sessions based on the provided criteria. - Enhanced error handling and logging for the purging process to improve debugging and user feedback. Remove debug print statement from purge_backtest_sessions function in BacktestSession model to streamline error handling and improve logging clarity. Add chart data retrieval endpoint for backtest sessions - Introduced a new API endpoint to fetch chart data for a specific backtest session. - Implemented validation for authorization tokens and session existence. - Enhanced data transformation to clean infinite values from chart data before returning it. - Updated the backtest session model to indicate the presence of chart data without returning it directly in the load more function.
1 parent 96378e7 commit ade0b0c

File tree

8 files changed

+771
-21
lines changed

8 files changed

+771
-21
lines changed

jesse/controllers/backtest_controller.py

Lines changed: 161 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11
from typing import Optional
2-
from fastapi import APIRouter, Header, Query
2+
from fastapi import APIRouter, Header, Query, Body
33
from fastapi.responses import JSONResponse, FileResponse
4-
4+
import json
55
from jesse.services import auth as authenticator
66
from jesse.services.multiprocessing import process_manager
7-
from jesse.services.web import BacktestRequestJson, CancelRequestJson
7+
from jesse.services.web import BacktestRequestJson, CancelRequestJson, UpdateBacktestSessionStateRequestJson, GetBacktestSessionsRequestJson, UpdateBacktestSessionNotesRequestJson
88
import jesse.helpers as jh
9+
from jesse.models.BacktestSession import (
10+
get_backtest_sessions as get_sessions,
11+
update_backtest_session_state,
12+
update_backtest_session_notes,
13+
delete_backtest_session,
14+
get_backtest_session_by_id as get_backtest_session_by_id_from_db
15+
)
16+
from jesse.services.transformers import get_backtest_session, get_backtest_session_for_load_more
917

1018
router = APIRouter(prefix="/backtest", tags=["Backtest"])
1119

@@ -53,6 +61,9 @@ def cancel_backtest(request_json: CancelRequestJson, authorization: Optional[str
5361
return authenticator.unauthorized_response()
5462

5563
process_manager.cancel_process(request_json.id)
64+
65+
from jesse.models.BacktestSession import update_backtest_session_status
66+
update_backtest_session_status(request_json.id, 'cancelled')
5667

5768
return JSONResponse({'message': f'Backtest process with ID of {request_json.id} was requested for termination'},
5869
status_code=202)
@@ -93,23 +104,158 @@ def download_backtest_log(session_id: str, token: str = Query(...)):
93104
return JSONResponse({'error': str(e)}, status_code=500)
94105

95106

107+
@router.post("/sessions")
108+
def get_backtest_sessions(request_json: GetBacktestSessionsRequestJson = Body(default=GetBacktestSessionsRequestJson()), authorization: Optional[str] = Header(None)):
109+
"""
110+
Get a list of backtest sessions sorted by most recently updated with pagination
111+
"""
112+
if not authenticator.is_valid_token(authorization):
113+
return authenticator.unauthorized_response()
96114

97-
@router.get("/logs/{session_id}")
98-
def get_logs(session_id: str, token: str = Query(...)):
115+
# Get sessions from the database with pagination and filters
116+
sessions = get_sessions(
117+
limit=request_json.limit,
118+
offset=request_json.offset,
119+
title_search=request_json.title_search,
120+
status_filter=request_json.status_filter,
121+
date_filter=request_json.date_filter
122+
)
123+
124+
# Transform the sessions using the transformer
125+
transformed_sessions = [get_backtest_session(session) for session in sessions]
126+
127+
return JSONResponse({
128+
'sessions': transformed_sessions,
129+
'count': len(transformed_sessions)
130+
})
131+
132+
133+
@router.post("/sessions/{session_id}")
134+
def get_backtest_session_by_id(session_id: str, authorization: Optional[str] = Header(None)):
99135
"""
100-
Get logs as text for a specific session. Similar to download but returns text content instead of file.
136+
Get a single backtest session by ID
101137
"""
102-
if not authenticator.is_valid_token(token):
138+
if not authenticator.is_valid_token(authorization):
103139
return authenticator.unauthorized_response()
104140

105-
try:
106-
from jesse.modes.data_provider import get_backtest_logs
107-
content = get_backtest_logs(session_id)
141+
# Get the session from the database
142+
session = get_backtest_session_by_id_from_db(session_id)
108143

109-
if content is None:
110-
return JSONResponse({'error': 'Log file not found'}, status_code=404)
144+
if not session:
145+
return JSONResponse({
146+
'error': f'Session with ID {session_id} not found'
147+
}, status_code=404)
111148

112-
return JSONResponse({'content': content}, status_code=200)
113-
except Exception as e:
114-
return JSONResponse({'error': str(e)}, status_code=500)
149+
# Transform the session using the transformer
150+
transformed_session = get_backtest_session_for_load_more(session)
151+
152+
return JSONResponse({
153+
'session': transformed_session
154+
})
155+
156+
157+
@router.post("/update-state")
158+
def update_session_state(request_json: UpdateBacktestSessionStateRequestJson, authorization: Optional[str] = Header(None)):
159+
"""
160+
Update the state of a backtest session
161+
"""
162+
if not authenticator.is_valid_token(authorization):
163+
return authenticator.unauthorized_response()
164+
165+
update_backtest_session_state(request_json.id, request_json.state)
166+
167+
return JSONResponse({
168+
'message': 'Backtest session state updated successfully'
169+
})
170+
171+
172+
@router.post("/sessions/{session_id}/remove")
173+
def remove_backtest_session(session_id: str, authorization: Optional[str] = Header(None)):
174+
"""
175+
Remove a backtest session from the database
176+
"""
177+
if not authenticator.is_valid_token(authorization):
178+
return authenticator.unauthorized_response()
179+
180+
session = get_backtest_session_by_id_from_db(session_id)
181+
182+
if not session:
183+
return JSONResponse({
184+
'error': f'Session with ID {session_id} not found'
185+
}, status_code=404)
186+
187+
# Delete the session from the database
188+
result = delete_backtest_session(session_id)
189+
190+
if not result:
191+
return JSONResponse({
192+
'error': f'Failed to delete session with ID {session_id}'
193+
}, status_code=500)
194+
195+
return JSONResponse({
196+
'message': 'Backtest session removed successfully'
197+
})
198+
199+
200+
@router.post("/sessions/{session_id}/notes")
201+
def update_session_notes(session_id: str, request_json: UpdateBacktestSessionNotesRequestJson, authorization: Optional[str] = Header(None)):
202+
"""
203+
Update the notes (title, description, strategy_codes) of a backtest session
204+
"""
205+
if not authenticator.is_valid_token(authorization):
206+
return authenticator.unauthorized_response()
207+
208+
session = get_backtest_session_by_id_from_db(session_id)
209+
210+
if not session:
211+
return JSONResponse({
212+
'error': f'Session with ID {session_id} not found'
213+
}, status_code=404)
214+
215+
update_backtest_session_notes(session_id, request_json.title, request_json.description, request_json.strategy_codes)
216+
217+
return JSONResponse({
218+
'message': 'Backtest session notes updated successfully'
219+
})
220+
221+
222+
@router.post("/purge-sessions")
223+
def purge_sessions(request_json: dict = Body(...), authorization: Optional[str] = Header(None)):
224+
"""
225+
Purge backtest sessions older than specified days
226+
"""
227+
if not authenticator.is_valid_token(authorization):
228+
return authenticator.unauthorized_response()
229+
230+
days_old = request_json.get('days_old', None)
231+
232+
from jesse.models.BacktestSession import purge_backtest_sessions
233+
deleted_count = purge_backtest_sessions(days_old)
234+
235+
return JSONResponse({
236+
'message': f'Successfully purged {deleted_count} session(s)',
237+
'deleted_count': deleted_count
238+
}, status_code=200)
239+
240+
241+
@router.post("/sessions/{session_id}/chart-data")
242+
def get_backtest_session_chart_data(session_id: str, authorization: Optional[str] = Header(None)):
243+
"""
244+
Get chart data for a specific backtest session
245+
"""
246+
if not authenticator.is_valid_token(authorization):
247+
return authenticator.unauthorized_response()
248+
249+
session = get_backtest_session_by_id_from_db(session_id)
250+
251+
if not session:
252+
return JSONResponse({
253+
'error': f'Session with ID {session_id} not found'
254+
}, status_code=404)
255+
256+
chart_data = jh.clean_infinite_values(json.loads(session.chart_data)) if session.chart_data else None
257+
258+
return JSONResponse({
259+
'chart_data': chart_data
260+
})
115261

jesse/helpers.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1123,6 +1123,28 @@ def clear_output():
11231123
click.clear()
11241124

11251125

1126+
def clean_infinite_values(obj):
1127+
"""
1128+
Recursively clean infinite values (inf, -inf) from data structures
1129+
by replacing them with None or 0
1130+
1131+
:param obj: The object to clean (can be dict, list, or primitive)
1132+
:return: The cleaned object with infinite values replaced
1133+
"""
1134+
import math
1135+
1136+
if isinstance(obj, dict):
1137+
return {k: clean_infinite_values(v) for k, v in obj.items()}
1138+
elif isinstance(obj, list):
1139+
return [clean_infinite_values(item) for item in obj]
1140+
elif isinstance(obj, float):
1141+
if math.isinf(obj):
1142+
return None
1143+
return obj
1144+
else:
1145+
return obj
1146+
1147+
11261148
def get_class_name(cls):
11271149
# if it's a string, return it
11281150
if isinstance(cls, str):

0 commit comments

Comments
 (0)