import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../models/backtest_history_entry_model.dart'; import '../models/backtest_report_model.dart'; import '../repositories/simulation_repository.dart'; class BacktestState extends Equatable { final bool isLoading; final BacktestReportModel? report; final String? errorMessage; /// True while [report] is a past run fetched via [BacktestCubit.viewHistoricalRun] rather than a freshly /// executed backtest - lets the UI label the summary/chart as "aus dem Verlauf" instead of implying a new /// run just completed. final bool isViewingHistoricalRun; final bool isHistoryLoading; final List history; final String? historyErrorMessage; /// Tunable indicator-parameter overrides for the currently selected strategy, keyed by bare parameter name /// (e.g. `"EmaFast"`, NOT `"TrendPullbackFvg.EmaFast"`) - the `"{strategyKey}."` prefix required by the /// backend (`TechnicalContext.ParameterOverrides`) is applied only when sending/loading, so this map stays /// meaningful regardless of which strategy is currently selected. Reset whenever the strategy changes (see /// [BacktestCubit.resetParameterOverrides]) - the same bare name means something different per strategy. final Map parameterOverrides; final bool isParametersLoading; final String? parametersMessage; const BacktestState({ this.isLoading = false, this.report, this.errorMessage, this.isViewingHistoricalRun = false, this.isHistoryLoading = false, this.history = const [], this.historyErrorMessage, this.parameterOverrides = const {}, this.isParametersLoading = false, this.parametersMessage, }); BacktestState copyWith({ bool? isLoading, BacktestReportModel? report, String? errorMessage, bool clearError = false, bool? isViewingHistoricalRun, bool? isHistoryLoading, List? history, String? historyErrorMessage, bool clearHistoryError = false, Map? parameterOverrides, bool? isParametersLoading, String? parametersMessage, bool clearParametersMessage = false, }) { return BacktestState( isLoading: isLoading ?? this.isLoading, report: report ?? this.report, errorMessage: clearError ? null : (errorMessage ?? this.errorMessage), isViewingHistoricalRun: isViewingHistoricalRun ?? this.isViewingHistoricalRun, isHistoryLoading: isHistoryLoading ?? this.isHistoryLoading, history: history ?? this.history, historyErrorMessage: clearHistoryError ? null : (historyErrorMessage ?? this.historyErrorMessage), parameterOverrides: parameterOverrides ?? this.parameterOverrides, isParametersLoading: isParametersLoading ?? this.isParametersLoading, parametersMessage: clearParametersMessage ? null : (parametersMessage ?? this.parametersMessage), ); } @override List get props => [ isLoading, report, errorMessage, isViewingHistoricalRun, isHistoryLoading, history, historyErrorMessage, parameterOverrides, isParametersLoading, parametersMessage, ]; } class BacktestCubit extends Cubit { final SimulationRepository repository; BacktestCubit({required this.repository}) : super(const BacktestState()); Future runBacktest({ required String isin, required String symbol, required String strategyKey, required String timeframe, }) async { emit(state.copyWith(isLoading: true, clearError: true, isViewingHistoricalRun: false)); try { final prefixedParams = state.parameterOverrides.isEmpty ? null : state.parameterOverrides.map((name, value) => MapEntry('$strategyKey.$name', value)); final report = await repository.runBacktest( isin: isin, symbol: symbol, strategyKey: strategyKey, timeframe: timeframe, strategyParameters: prefixedParams, ); emit(state.copyWith(isLoading: false, report: report, clearError: true, isViewingHistoricalRun: false)); // The run that just completed is now the newest history entry - refresh so it shows up immediately // instead of the user only seeing it after manually reopening the history list. await loadHistory(isin: isin, strategyKey: strategyKey); } catch (e) { emit(state.copyWith(isLoading: false, errorMessage: 'Fehler beim Starten des Backtests: $e')); } } Future loadHistory({required String isin, String? strategyKey}) async { if (isin.trim().isEmpty) { emit(state.copyWith(history: const [], clearHistoryError: true)); return; } emit(state.copyWith(isHistoryLoading: true, clearHistoryError: true)); try { final history = await repository.getBacktestHistory(isin: isin, strategyKey: strategyKey); emit(state.copyWith(isHistoryLoading: false, history: history, clearHistoryError: true)); } catch (e) { emit(state.copyWith(isHistoryLoading: false, historyErrorMessage: 'Verlauf konnte nicht geladen werden: $e')); } } Future viewHistoricalRun(String runId) async { emit(state.copyWith(isLoading: true, clearError: true)); try { final report = await repository.getBacktestRunDetail(runId); emit(state.copyWith(isLoading: false, report: report, clearError: true, isViewingHistoricalRun: true)); } catch (e) { emit(state.copyWith(isLoading: false, errorMessage: 'Backtest-Lauf konnte nicht geladen werden: $e')); } } /// Sets a single tunable-parameter override (bare name, e.g. `"EmaFast"`) for the currently selected strategy. void setParameterOverride(String paramName, double value) { final updated = Map.from(state.parameterOverrides); updated[paramName] = value; emit(state.copyWith(parameterOverrides: updated, clearParametersMessage: true)); } /// Clears all overrides - called whenever the selected strategy changes, since a bare parameter name means /// something different per strategy (e.g. `"Period"` is an RSI period for one strategy, a Donchian-channel /// length for another). void resetParameterOverrides() { emit(state.copyWith(parameterOverrides: const {}, clearParametersMessage: true)); } /// Loads a previously saved parameter profile for (isin, strategyKey) into [BacktestState.parameterOverrides]. Future loadSavedParameters({required String isin, required String strategyKey}) async { emit(state.copyWith(isParametersLoading: true, clearParametersMessage: true)); try { final saved = await repository.getStrategyParameters(isin: isin, strategyKey: strategyKey); if (saved == null) { emit(state.copyWith( isParametersLoading: false, parametersMessage: 'Kein gespeichertes Profil für dieses Asset/diese Strategie.', )); return; } final prefix = '$strategyKey.'; final bare = {}; saved.forEach((key, value) { if (key.startsWith(prefix)) bare[key.substring(prefix.length)] = value; }); emit(state.copyWith( isParametersLoading: false, parameterOverrides: bare, parametersMessage: 'Gespeichertes Profil geladen.', )); } catch (e) { emit(state.copyWith(isParametersLoading: false, parametersMessage: 'Fehler beim Laden des Profils: $e')); } } /// Saves the current [BacktestState.parameterOverrides] as a reusable profile for (isin, strategyKey). Future saveCurrentParameters({required String isin, required String strategyKey}) async { emit(state.copyWith(isParametersLoading: true, clearParametersMessage: true)); try { final prefixed = state.parameterOverrides.map((name, value) => MapEntry('$strategyKey.$name', value)); await repository.saveStrategyParameters(isin: isin, strategyKey: strategyKey, parameters: prefixed); emit(state.copyWith(isParametersLoading: false, parametersMessage: 'Parameter-Profil gespeichert.')); } catch (e) { emit(state.copyWith(isParametersLoading: false, parametersMessage: 'Fehler beim Speichern des Profils: $e')); } } }