• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

freqtrade / freqtrade / 6181253459

08 Sep 2023 06:04AM UTC coverage: 94.614% (+0.06%) from 94.556%
6181253459

push

github-actions

web-flow
Merge pull request #9159 from stash86/fix-adjust

remove old codes when we only can do partial entries

2 of 2 new or added lines in 1 file covered. (100.0%)

19114 of 20202 relevant lines covered (94.61%)

0.95 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.24
/freqtrade/rpc/api_server/api_backtest.py
1
import asyncio
1✔
2
import logging
1✔
3
from copy import deepcopy
1✔
4
from datetime import datetime
1✔
5
from pathlib import Path
1✔
6
from typing import Any, Dict, List
1✔
7

8
from fastapi import APIRouter, BackgroundTasks, Depends
1✔
9
from fastapi.exceptions import HTTPException
1✔
10

11
from freqtrade.configuration.config_validation import validate_config_consistency
1✔
12
from freqtrade.constants import Config
1✔
13
from freqtrade.data.btanalysis import (delete_backtest_result, get_backtest_result,
1✔
14
                                       get_backtest_resultlist, load_and_merge_backtest_result,
15
                                       update_backtest_metadata)
16
from freqtrade.enums import BacktestState
1✔
17
from freqtrade.exceptions import DependencyException, OperationalException
1✔
18
from freqtrade.exchange.common import remove_exchange_credentials
1✔
19
from freqtrade.misc import deep_merge_dicts, is_file_in_dir
1✔
20
from freqtrade.rpc.api_server.api_schemas import (BacktestHistoryEntry, BacktestMetadataUpdate,
1✔
21
                                                  BacktestRequest, BacktestResponse)
22
from freqtrade.rpc.api_server.deps import get_config
1✔
23
from freqtrade.rpc.api_server.webserver_bgwork import ApiBG
1✔
24
from freqtrade.rpc.rpc import RPCException
1✔
25
from freqtrade.types import get_BacktestResultType_default
1✔
26

27

28
logger = logging.getLogger(__name__)
1✔
29

30
# Private API, protected by authentication and webserver_mode dependency
31
router = APIRouter()
1✔
32

33

34
def __run_backtest_bg(btconfig: Config):
1✔
35
    from freqtrade.optimize.optimize_reports import generate_backtest_stats, store_backtest_stats
1✔
36
    from freqtrade.resolvers import StrategyResolver
1✔
37
    asyncio.set_event_loop(asyncio.new_event_loop())
1✔
38
    try:
1✔
39
        # Reload strategy
40
        lastconfig = ApiBG.bt['last_config']
1✔
41
        strat = StrategyResolver.load_strategy(btconfig)
1✔
42
        validate_config_consistency(btconfig)
1✔
43

44
        if (
1✔
45
            not ApiBG.bt['bt']
46
            or lastconfig.get('timeframe') != strat.timeframe
47
            or lastconfig.get('timeframe_detail') != btconfig.get('timeframe_detail')
48
            or lastconfig.get('timerange') != btconfig['timerange']
49
        ):
50
            from freqtrade.optimize.backtesting import Backtesting
1✔
51
            ApiBG.bt['bt'] = Backtesting(btconfig)
1✔
52
            ApiBG.bt['bt'].load_bt_data_detail()
1✔
53
        else:
54
            ApiBG.bt['bt'].config = btconfig
1✔
55
            ApiBG.bt['bt'].init_backtest()
1✔
56
        # Only reload data if timeframe changed.
57
        if (
1✔
58
            not ApiBG.bt['data']
59
            or not ApiBG.bt['timerange']
60
            or lastconfig.get('timeframe') != strat.timeframe
61
            or lastconfig.get('timerange') != btconfig['timerange']
62
        ):
63
            ApiBG.bt['data'], ApiBG.bt['timerange'] = ApiBG.bt[
1✔
64
                'bt'].load_bt_data()
65

66
        lastconfig['timerange'] = btconfig['timerange']
1✔
67
        lastconfig['timeframe'] = strat.timeframe
1✔
68
        lastconfig['protections'] = btconfig.get('protections', [])
1✔
69
        lastconfig['enable_protections'] = btconfig.get('enable_protections')
1✔
70
        lastconfig['dry_run_wallet'] = btconfig.get('dry_run_wallet')
1✔
71

72
        ApiBG.bt['bt'].enable_protections = btconfig.get('enable_protections', False)
1✔
73
        ApiBG.bt['bt'].strategylist = [strat]
1✔
74
        ApiBG.bt['bt'].results = get_BacktestResultType_default()
1✔
75
        ApiBG.bt['bt'].load_prior_backtest()
1✔
76

77
        ApiBG.bt['bt'].abort = False
1✔
78
        strategy_name = strat.get_strategy_name()
1✔
79
        if (ApiBG.bt['bt'].results and
1✔
80
                strategy_name in ApiBG.bt['bt'].results['strategy']):
81
            # When previous result hash matches - reuse that result and skip backtesting.
82
            logger.info(f'Reusing result of previous backtest for {strategy_name}')
1✔
83
        else:
84
            min_date, max_date = ApiBG.bt['bt'].backtest_one_strategy(
1✔
85
                strat, ApiBG.bt['data'], ApiBG.bt['timerange'])
86

87
            ApiBG.bt['bt'].results = generate_backtest_stats(
1✔
88
                ApiBG.bt['data'], ApiBG.bt['bt'].all_results,
89
                min_date=min_date, max_date=max_date)
90

91
        if btconfig.get('export', 'none') == 'trades':
1✔
92
            fn = store_backtest_stats(
1✔
93
                btconfig['exportfilename'], ApiBG.bt['bt'].results,
94
                datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
95
                )
96
            ApiBG.bt['bt'].results['metadata'][strategy_name]['filename'] = str(fn.name)
1✔
97
            ApiBG.bt['bt'].results['metadata'][strategy_name]['strategy'] = strategy_name
1✔
98

99
        logger.info("Backtest finished.")
1✔
100

101
    except (Exception, OperationalException, DependencyException) as e:
1✔
102
        logger.exception(f"Backtesting caused an error: {e}")
1✔
103
        ApiBG.bt['bt_error'] = str(e)
1✔
104
        pass
1✔
105
    finally:
106
        ApiBG.bgtask_running = False
1✔
107

108

109
@router.post('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest'])
1✔
110
async def api_start_backtest(
1✔
111
        bt_settings: BacktestRequest, background_tasks: BackgroundTasks,
112
        config=Depends(get_config)):
113
    ApiBG.bt['bt_error'] = None
1✔
114
    """Start backtesting if not done so already"""
1✔
115
    if ApiBG.bgtask_running:
1✔
116
        raise RPCException('Bot Background task already running')
1✔
117

118
    if ':' in bt_settings.strategy:
1✔
119
        raise HTTPException(status_code=500, detail="base64 encoded strategies are not allowed.")
1✔
120

121
    btconfig = deepcopy(config)
1✔
122
    remove_exchange_credentials(btconfig['exchange'], True)
1✔
123
    settings = dict(bt_settings)
1✔
124
    if settings.get('freqai', None) is not None:
1✔
125
        settings['freqai'] = dict(settings['freqai'])
×
126
    # Pydantic models will contain all keys, but non-provided ones are None
127

128
    btconfig = deep_merge_dicts(settings, btconfig, allow_null_overrides=False)
1✔
129
    try:
1✔
130
        btconfig['stake_amount'] = float(btconfig['stake_amount'])
1✔
131
    except ValueError:
×
132
        pass
×
133

134
    # Force dry-run for backtesting
135
    btconfig['dry_run'] = True
1✔
136

137
    # Start backtesting
138
    # Initialize backtesting object
139

140
    background_tasks.add_task(__run_backtest_bg, btconfig=btconfig)
1✔
141
    ApiBG.bgtask_running = True
1✔
142

143
    return {
1✔
144
        "status": "running",
145
        "running": True,
146
        "progress": 0,
147
        "step": str(BacktestState.STARTUP),
148
        "status_msg": "Backtest started",
149
    }
150

151

152
@router.get('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest'])
1✔
153
def api_get_backtest():
1✔
154
    """
155
    Get backtesting result.
156
    Returns Result after backtesting has been ran.
157
    """
158
    from freqtrade.persistence import LocalTrade
1✔
159
    if ApiBG.bgtask_running:
1✔
160
        return {
1✔
161
            "status": "running",
162
            "running": True,
163
            "step": (ApiBG.bt['bt'].progress.action if ApiBG.bt['bt']
164
                     else str(BacktestState.STARTUP)),
165
            "progress": ApiBG.bt['bt'].progress.progress if ApiBG.bt['bt'] else 0,
166
            "trade_count": len(LocalTrade.trades),
167
            "status_msg": "Backtest running",
168
        }
169

170
    if not ApiBG.bt['bt']:
1✔
171
        return {
1✔
172
            "status": "not_started",
173
            "running": False,
174
            "step": "",
175
            "progress": 0,
176
            "status_msg": "Backtest not yet executed"
177
        }
178
    if ApiBG.bt['bt_error']:
1✔
179
        return {
1✔
180
            "status": "error",
181
            "running": False,
182
            "step": "",
183
            "progress": 0,
184
            "status_msg": f"Backtest failed with {ApiBG.bt['bt_error']}"
185
        }
186

187
    return {
1✔
188
        "status": "ended",
189
        "running": False,
190
        "status_msg": "Backtest ended",
191
        "step": "finished",
192
        "progress": 1,
193
        "backtest_result": ApiBG.bt['bt'].results,
194
    }
195

196

197
@router.delete('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest'])
1✔
198
def api_delete_backtest():
1✔
199
    """Reset backtesting"""
200
    if ApiBG.bgtask_running:
1✔
201
        return {
1✔
202
            "status": "running",
203
            "running": True,
204
            "step": "",
205
            "progress": 0,
206
            "status_msg": "Backtest running",
207
        }
208
    if ApiBG.bt['bt']:
1✔
209
        ApiBG.bt['bt'].cleanup()
1✔
210
        del ApiBG.bt['bt']
1✔
211
        ApiBG.bt['bt'] = None
1✔
212
        del ApiBG.bt['data']
1✔
213
        ApiBG.bt['data'] = None
1✔
214
        logger.info("Backtesting reset")
1✔
215
    return {
1✔
216
        "status": "reset",
217
        "running": False,
218
        "step": "",
219
        "progress": 0,
220
        "status_msg": "Backtest reset",
221
    }
222

223

224
@router.get('/backtest/abort', response_model=BacktestResponse, tags=['webserver', 'backtest'])
1✔
225
def api_backtest_abort():
1✔
226
    if not ApiBG.bgtask_running:
1✔
227
        return {
1✔
228
            "status": "not_running",
229
            "running": False,
230
            "step": "",
231
            "progress": 0,
232
            "status_msg": "Backtest ended",
233
        }
234
    ApiBG.bt['bt'].abort = True
1✔
235
    return {
1✔
236
        "status": "stopping",
237
        "running": False,
238
        "step": "",
239
        "progress": 0,
240
        "status_msg": "Backtest ended",
241
    }
242

243

244
@router.get('/backtest/history', response_model=List[BacktestHistoryEntry],
1✔
245
            tags=['webserver', 'backtest'])
246
def api_backtest_history(config=Depends(get_config)):
1✔
247
    # Get backtest result history, read from metadata files
248
    return get_backtest_resultlist(config['user_data_dir'] / 'backtest_results')
1✔
249

250

251
@router.get('/backtest/history/result', response_model=BacktestResponse,
1✔
252
            tags=['webserver', 'backtest'])
253
def api_backtest_history_result(filename: str, strategy: str, config=Depends(get_config)):
1✔
254
    # Get backtest result history, read from metadata files
255
    bt_results_base: Path = config['user_data_dir'] / 'backtest_results'
1✔
256
    fn = (bt_results_base / filename).with_suffix('.json')
1✔
257

258
    results: Dict[str, Any] = {
1✔
259
        'metadata': {},
260
        'strategy': {},
261
        'strategy_comparison': [],
262
    }
263
    if not is_file_in_dir(fn, bt_results_base):
1✔
264
        raise HTTPException(status_code=404, detail="File not found.")
×
265
    load_and_merge_backtest_result(strategy, fn, results)
1✔
266
    return {
1✔
267
        "status": "ended",
268
        "running": False,
269
        "step": "",
270
        "progress": 1,
271
        "status_msg": "Historic result",
272
        "backtest_result": results,
273
    }
274

275

276
@router.delete('/backtest/history/{file}', response_model=List[BacktestHistoryEntry],
1✔
277
               tags=['webserver', 'backtest'])
278
def api_delete_backtest_history_entry(file: str, config=Depends(get_config)):
1✔
279
    # Get backtest result history, read from metadata files
280
    bt_results_base: Path = config['user_data_dir'] / 'backtest_results'
1✔
281
    file_abs = (bt_results_base / file).with_suffix('.json')
1✔
282
    # Ensure file is in backtest_results directory
283
    if not is_file_in_dir(file_abs, bt_results_base):
1✔
284
        raise HTTPException(status_code=404, detail="File not found.")
1✔
285

286
    delete_backtest_result(file_abs)
1✔
287
    return get_backtest_resultlist(config['user_data_dir'] / 'backtest_results')
1✔
288

289

290
@router.patch('/backtest/history/{file}', response_model=List[BacktestHistoryEntry],
1✔
291
              tags=['webserver', 'backtest'])
292
def api_update_backtest_history_entry(file: str, body: BacktestMetadataUpdate,
1✔
293
                                      config=Depends(get_config)):
294
    # Get backtest result history, read from metadata files
295
    bt_results_base: Path = config['user_data_dir'] / 'backtest_results'
1✔
296
    file_abs = (bt_results_base / file).with_suffix('.json')
1✔
297
    # Ensure file is in backtest_results directory
298
    if not is_file_in_dir(file_abs, bt_results_base):
1✔
299
        raise HTTPException(status_code=404, detail="File not found.")
1✔
300
    content = {
1✔
301
        'notes': body.notes
302
    }
303
    try:
1✔
304
        update_backtest_metadata(file_abs, body.strategy, content)
1✔
305
    except ValueError as e:
1✔
306
        raise HTTPException(status_code=400, detail=str(e))
1✔
307

308
    return get_backtest_result(file_abs)
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc